Java面试篇-AOP专题(什么是AOP、AOP的几个核心概念、AOP的常用场景、使用AOP记录操作日志、Spring中的事务是如何实现的)

news/2024/9/23 3:28:44 标签: spring, java, 面试

文章目录

  • 1. 什么是AOP
  • 2. AOP的几个核心概念
  • 3. AOP的常用场景
  • 4. 使用AOP记录操作日志
    • 4.1 准备工作
      • 4.1.1 引入Maven依赖
      • 4.1.2 UserController.java
      • 4.1.3 User.java
      • 4.1.4 UserService.java
    • 4.2 具体实现(以根据id查询用户信息为例)
      • 4.2.1 定义切面类(切入点和环绕增强)
      • 4.2.2 自定义注解
      • 4.2.3 为方法添加自定义注解
    • 4.3 测试
  • 5. Spring中的事务是如何实现的

1. 什么是AOP

AOP(Aspect Oriented Programming),面向切面编程

AOP 主要的功能是将 与业务无关,但却对多个对象产生影响的公共行为或逻辑 抽取并封装为一个可重用的模块,这个可重用的模块称为切面(Aspect)

AOP 能够减少系统中的重复代码,降低模块间的耦合度,同时提高系统的可维护性


如果想了解 Spring 事务失效的情况,可以参考我的另一篇博文:Spring中事务失效的常见场景及解决方法

2. AOP的几个核心概念

AOP 的核心概念主要包括以下几个:

  1. 切面(Aspect)
    • 切面是AOP中的一个核心概念,它代表了一个横切关注点(cross-cutting concern),即将多个模块中共有的行为抽象出来形成的一个独立模块。在Spring AOP中,切面通常是通过使用@Aspect注解的类来实现的
  2. 连接点(Join Point)
    • 连接点是在程序执行过程中的一个特定点,例如方法的调用、异常的抛出等。在Spring AOP中,只支持方法的连接点
  3. 切入点(Pointcut)
    • 切入点是一组连接点的定义,它定义了哪些连接点会被切面所拦截。通常使用正则表达式或者特定的表达式语言来指定哪些方法会被拦截
  4. 通知(Advice)
    • 通知定义了切面在特定的连接点上要执行的动作。通知有多种类型:
      • 前置通知(Before):在连接点之前执行
      • 后置通知(After):在连接点之后执行,无论方法是否正常结束
      • 返回通知(After Returning):在连接点正常返回后执行
      • 异常通知(After Throwing):在连接点抛出异常后执行
      • 环绕通知(Around):包围一个连接点的通知,可以在方法调用前后执行自定义的行为
  5. 目标对象(Target Object)
    • 目标对象是指被一个或多个切面所通知的对象。在Spring AOP中,目标对象通常是Spring容器中的Bean
  6. 代理(Proxy)
    • AOP通过代理模式来实现对目标对象的增强。代理对象会在运行时创建,并用来代替目标对象。当调用代理对象的方法时,代理会根据切面的配置来执行相应的通知
  7. 织入(Weaving)
    • 织入是将切面的通知应用到目标对象并创建新的代理对象的过程。这个过程可以在编译时、类加载时或运行时进行

3. AOP的常用场景

  1. 记录操作日志:记录日志属于公共的行为,每一个 Service 都需要记录操作日志,直接在每一个 Service 里面编写记录日志的代码不太合适,使用 AOP 就可以很方便地完成记录日志操作
  2. 处理缓存:如果某些业务要添加缓存,直接写在 Service 层会造成代码耦合的情况,我们可以利用 AOP 的切面,拦截需要添加缓存的业务方法,为业务方法添加缓存
  3. Spring 中内置的事务处理

4. 使用AOP记录操作日志

4.1 准备工作

4.1.1 引入Maven依赖

在 SpringBoot 项目的 pom.xml 文件中引入 aspectjweaver 的 Maven 依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

java_62">4.1.2 UserController.java

在这里插入图片描述

java">import cn.edu.scau.aop.pojo.User;
import cn.edu.scau.aop.service.UserService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/getById/{id}")
    public User getById(@PathVariable("id") Integer id) {
        return userService.getById(id);
    }

    @PostMapping("/save")
    public void save(@RequestBody User user) {
        userService.save(user);
    }

    @PutMapping("/update")
    public void update(User user) {
        userService.update(user);
    }

    @DeleteMapping("/delete/{id}")
    public void delete(@PathVariable("id") Integer id) {
        userService.delete(id);
    }

}

java_105">4.1.3 User.java

java">public class User {

    private Integer id;

    private String name;

    private Integer age;

    private Short sex;

    private Short status;

    private String image;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Short getSex() {
        return sex;
    }

    public void setSex(Short sex) {
        this.sex = sex;
    }

    public Short getStatus() {
        return status;
    }

    public void setStatus(Short status) {
        this.status = status;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", sex=" + sex +
                ", status=" + status +
                ", image='" + image + '\'' +
                '}';
    }
    
}

java_185">4.1.4 UserService.java

java">import cn.edu.scau.aop.pojo.User;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    public User getById(Integer id) {
        return new User();
    }

    public void save(User user) {
        System.out.println("保存用户信息");
    }

    public void update(User user) {
        System.out.println("更新用户信息");
    }

    public void delete(Integer id) {
        System.out.println("删除用户信息");
    }

}

4.2 具体实现(以根据id查询用户信息为例)

在我们的开发过程中,大多都有记录操作日志的需求

在这里插入图片描述

当用户访问某一个接口时,我们需要记录发起请求的用户是谁,请求的方式是什么,访问地址是什么,访问了哪一个模块,登录的 IP 地址,操作时间等


接下来我们分析一下利用 AOP 记录操作日志的具体实现方式

假如后台有四个请求的接口:登录、新增用户、更新用户、删除用户

在这里插入图片描述

我们以查询用户为例,利用 AOP 提供的环绕通知做一个切面

在这里插入图片描述

4.2.1 定义切面类(切入点和环绕增强)

用 @Aspect 注解表名当前类是一个切面类,而且切面类需要交由 Spring 管理

java">import cn.edu.scau.aop.annotation.Log;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;
import java.util.Date;

/**
 * 切面类
 */
@Component
@Aspect
public class SystemAspect {

    @Pointcut("@annotation(cn.edu.scau.aop.annotation.Log)")
    private void pointcut() {

    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 通过解析 session 或 token 获取用户名

        // 获取被增强类和方法的信息
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;

        // 获取被增强的方法对象
        Method method = methodSignature.getMethod();

        // 从方法中解析注解
        if (method != null) {
            Log logAnnotation = method.getAnnotation(Log.class);
            System.out.println(logAnnotation.name());
        }

        // 方法名字
        String name = null;
        if (method != null) {
            name = method.getName();
        }
        System.out.println("方法名:" + name);

        // 通过工具类获取Request对象
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
        HttpServletRequest request = null;
        if (servletRequestAttributes != null) {
            request = servletRequestAttributes.getRequest();
        }

        // 访问的URL
        String url = null;
        if (request != null) {
            url = request.getRequestURI();
        }
        System.out.println("访问的URL:" + url);

        // 请求方式
        String methodName = null;
        if (request != null) {
            methodName = request.getMethod();
        }
        System.out.println("请求方式:" + methodName);

        // 登录IP
        String ipAddress = null;
        if (request != null) {
            ipAddress = getIpAddress(request);
        }
        System.out.println("登录IP:" + ipAddress);

        // 操作时间
        System.out.println("操作时间:" + new Date());

        // 将操作日志保存到数据库

        return joinPoint.proceed();
    }

    /**
     * 获取 IP 地址
     *
     * @param request HttpServletRequest
     * @return String
     */
    public String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");

        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }

        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }

}

切面类中有一个切点表达式

在这里插入图片描述

这个切点表达式找的是一个注解,也就是说,如果某个方法上添加了 Log 注解,进化就会进入到环绕通知中进行增强


环绕通知增强如下

在这里插入图片描述

java">@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    // 通过解析 session 或 token 获取用户名

    // 获取被增强类和方法的信息
    Signature signature = joinPoint.getSignature();
    MethodSignature methodSignature = (MethodSignature) signature;

    // 获取被增强的方法对象
    Method method = methodSignature.getMethod();

    // 从方法中解析注解
    if (method != null) {
        Log logAnnotation = method.getAnnotation(Log.class);
        System.out.println("模块名称:" + logAnnotation.name());
    }

    // 方法名字
    String name = null;
    if (method != null) {
        name = method.getName();
    }
    System.out.println("方法名:" + name);

    // 通过工具类获取Request对象
    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
    HttpServletRequest request = null;
    if (servletRequestAttributes != null) {
        request = servletRequestAttributes.getRequest();
    }

    // 访问的URL
    String url = null;
    if (request != null) {
        url = request.getRequestURI();
    }
    System.out.println("访问的URL:" + url);

    // 请求方式
    String methodName = null;
    if (request != null) {
        methodName = request.getMethod();
    }
    System.out.println("请求方式:" + methodName);

    // 登录IP
    String ipAddress = null;
    if (request != null) {
        ipAddress = getIpAddress(request);
    }
    System.out.println("登录IP:" + ipAddress);

    // 操作时间
    System.out.println("操作时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

    // 将操作日志保存到数据库

    return joinPoint.proceed();
}

/**
 * 获取 IP 地址
 *
 * @param request HttpServletRequest
 * @return String
 */
public String getIpAddress(HttpServletRequest request) {
    String ip = request.getHeader("x-forwarded-for");

    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getHeader("Proxy-Client-IP");
    }
    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getHeader("WL-Proxy-Client-IP");
    }
    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getRemoteAddr();
    }

    return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}

4.2.2 自定义注解

那 Log 注解是从哪里来的呢,其实是我们自定义的,注解中的 name 属性是模块的名称

在这里插入图片描述

java">import java.lang.annotation.*;

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {

    /**
     * 模块名称
     */
    String name() default "";

}

4.2.3 为方法添加自定义注解

我们在需要记录操作日志的方法上添加自定义注解

在这里插入图片描述

4.3 测试

启动项目后,我们在浏览器输入以下网址访问接口

http://localhost:8080/user/getById/1

查看控制台,发现操作日志已成功打印操作日志

在这里插入图片描述

5. Spring中的事务是如何实现的

Spring支持编程式事务管理和声明式事务管理两种方式

  • 编程式事务控制:需使用 TransactionTemplate 来进行实现,对业务代码有侵入性,项目中很少使用
  • 声明式事务管理:声明式事务管理建立在 AOP 之上,本质是通过 AOP 对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务

声明式事务的示意图

在这里插入图片描述

joinPoint.proceed 是真正要执行的目标对象的方法,在方法执行前开启事务,方法成功执行之后提交事务

如果方法在执行的过程中出错了,需要回滚事务,在 catch 代码块中会有一个回滚事务的操作


http://www.niftyadmin.cn/n/5671207.html

相关文章

JavaEE:探索网络世界的魅力——玩转UDP编程

文章目录 UDPUDP的特点UDP协议端格式校验和前置知识校验和具体是如何工作的? UDP UDP的特点 UDP传输的过程类似于寄信. 无连接: 知道对端的IP和端口号就直接进行传输,不需要建立连接.不可靠: 没有确认机制,没有重传机制,如果因为网络故障导致该段无法到达对方,UDP协议也不会…

java项目之常规应急物资管理系统(源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的常规应急物资管理系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息。 项目简介&#xff1a; 基于SpringBootVue的…

网络丢包定位记录(一)

数据在Internet上是以数据包为单位传输的&#xff0c;单位为字节&#xff0c;数据在网络上传输&#xff0c;受网络设备&#xff0c;网络质量等原因的影响&#xff0c;使得接收到的数据少于发送出去的数据&#xff0c;造成丢包。 数据包接收、发送原理 发送数据包&#xff1a; …

C++标准库容器类——string类

引言 在c中&#xff0c;string类的引用极大地简化了字符串的操作和管理&#xff0c;相比 C 风格字符串&#xff08;char*或cahr[]&#xff09;&#xff0c;std::string 提供了更高效和更安全的字符串操作。接下来让我们一起来深入学习string类吧&#xff01; 1.string 的构造…

Java | Leetcode Java题解之第414题第三大的数

题目&#xff1a; 题解&#xff1a; class Solution {public int thirdMax(int[] nums) {Integer a null, b null, c null;for (int num : nums) {if (a null || num > a) {c b;b a;a num;} else if (a > num && (b null || num > b)) {c b;b num;…

SecureCRT下载

文章目录 1.下载链接2.说明 1.下载链接 通过百度网盘分享的文件&#xff1a; 通过百度网盘分享的文件&#xff1a;securecrt8.0安装包&配色主题&永久激活 链接&#xff1a;https://pan.baidu.com/s/1NpkjFUIpKDf9VcSYhmumrg? 提取码&#xff1a;https://item.taobao…

C++面向对象:多态!

前言 多态是面向对象三大基本特性其一&#xff0c;多态可以实现“一个接口&#xff0c;多个方法”。 一.多态的基本概念 在使用多态时&#xff0c;不同的对象完成同一件事可能会有不同的结果。 如下例&#xff1a;买地铁票时&#xff0c;普通人全价&#xff0c;学生半价&am…

等保测评:企业如何构建安全的网络架构

等保测评的目的和重要性 等保测评&#xff08;信息安全等级保护测评&#xff09;是企业构建安全网络架构的重要依据。它通过对信息系统的安全等级进行评估&#xff0c;帮助企业识别潜在的安全风险&#xff0c;提供科学的安全保护建议&#xff0c;确保网络系统的稳定和可靠运行。…