Spring Boot 如何使用 Spring AOP

面向切面编程

面向切面编程(Aspect Oriented Programming),可以将与业务无关但是被各个业务模块共同调用的逻辑抽取出来,以切面的方式切入到代码中,从而降低系统中代码的耦合度,减少重复的代码。

Spring AOP 是通过预编译方式运行期间动态代理实现程序面向切面编程。

试想我们的项目中有一个接口,它的代码逻辑是这样的:

public R api() {
    查询数据库;
    返回数据;
}

现在我们需要对该接口进行登录验证,只有登录了的用户才能访问该接口,如果用户没有登录,那么返回一个错误结果。此时,最简单的方式就是使用 if-else 进行判断,添加到代码逻辑中。但如果这种接口数量一多,那我们的工作量就势必加大了。

如果后续开发中,我们还需要给接口添加权限验证,只有具有某种权限的用户才能访问接口,那我们又需要添加大量重复代码。

这种应用场景,例如登录校验、权限校验、日志处理等这种多个模块可能会共同调用的代码,我们完全可以使用切面的方式,将逻辑切入到业务模块中。

AOP 的底层实现原理

AOP 底层使用动态代理完成需求,为需要增加增强功能的类生成代理类,有两种生成代理类的方式,对于被代理类(即需要增强的类),如果:

  1. 实现了接口,使用 JDK 动态代理,生成的代理类会使用其接口
  2. 没有实现接口,使用 CGlib 动态代理,生成的代理类会继承被代理类

简单看看 JDK 动态代理的实现方式,可以看到使用了设计模式-代理模式:

// 我们定义一个接口,声明一个登录功能的方法 public interface UserService { void login(String username, String password);
} // 有一个实现类,实现登录功能 public class UserServiceImpl implements UserService{ @Override public void login(String username, String password) {
        System.out.println("登录功能, username="+ username + ",password=" + password);
    }
} // 创建一个代理类,完成代理,增强被代理类的功能 public class UserServiceProxy implements InvocationHandler { // 被代理类的实例,传递进来的就是 UserServiceImpl 的实例 private Object obj; public UserServiceProxy(Object obj) { this.obj = obj;
    } // 定义如何增强功能 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("login")) {
            System.out.println("执行主体功能之前,增强功能.......");
            System.out.println("执行方法:" + method.getName() + ",方法参数: {" + Arrays.toString(args) + "}"); // 增强功能:给用户名添加后缀,实际情况中,可能我们可以判断以下请求的 IP 地址是否在运行范围内 args[0] += "123123"; // 如果我们直接 return method.invoke, 不编写其他代码,那么就等于没有增强功能 // 调用 method.invoke 就是方法执行后的返回结果,如果不调用 method.invoke,就不会执行主体功能 Object res = method.invoke(obj, args);
            System.out.println("执行主体功能之后,增强功能......."); return res;
        } return method.invoke(obj, args);
    }
} // 测试: public class Main { public static void main(String[] args) {
        Class[] interfaces = {UserService.class};

        UserServiceImpl userServiceImpl = new UserServiceImpl();
        UserService userService = (UserService) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), interfaces, new UserServiceProxy(userServiceImpl));
        userService.login("username", "123123");
    }
} //======================================= 运行结果 执行主体功能之前,增强功能.......
执行方法:login,方法参数: {[username, 123123]}
登录功能, username=username123123,password=123123 执行主体功能之后,增强功能.......

AOP 的相关术语

  • 连接点:被代理(被增强)的类中的方法
  • 切入点:实际上需要被增强的方法
  • 通知:要增强的逻辑代码
    • 前置通知:在主体功能执行之前执行
    • 后置通知:在主体功能执行之后执行
    • 环绕通知:在主体功能执行前后执行
    • 异常通知:在主体功能执行出现异常时执行
    • 最终通知:主体功能无论执行是否成功都会执行
  • 切面:切入点和切面的结合,即被增强的方法和增强的功能组成切面

Spring Boot 使用 Spring AOP

接下来看看在 Spring Boot 中如何使用 Spring AOP。

首先引入一个 spring-boot-starter-aop

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> 

在实际开发中,我们可以使用切入点表达式声明切入点,如:

  • execution([权限修饰符] [返回类型] [类全路径].[方法名称] ([参数类型列表])
    • execution(* com.xxx.ABC.add()), 对 ABC 类的方法进行增强
  • @annotaion(注解)标注了指定注解的方法
  • @bean(beanName)指定的 beanName 的 bean 的方***被增强

切入点表达式可以用上 || 、&& 逻辑运算符。

我们会使用到如下几个注解:


我们编写两个类:


代码如下:

//=================================== 切面代码 =================================== @Component // 这是一个组件,会交由 IOC 容器管理 @Aspect // 这个类是一个切面 public class TestAOP { // 切入点表达式,TestController 下的 test 方法为切入点 public static final String EXECUTION = "execution(public void com.example.aopdemo.controller.TestController.test())"; // 也可以这样使用 @Before("execution(public void com.example.aopdemo.controller.TestController.test())") @Before(EXECUTION) public void before() {
        System.out.println("前置通知");
    } // 切入点的另一种编写方式,具体使用查看第【20】行 @Pointcut(value="execution(public void com.example.aopdemo.controller.TestController.test())") public void pointCut() {
    } @AfterReturning("pointCut()") public void afterReturning() {
        System.out.println("后置通知");
    } // ProceedingJoinPoint 实例含有切入点的信息,可以获取方法签名,参数列表等 // 环绕通知使用这个对象实例执行切入点的功能 @Around(EXECUTION) public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知之前"); // 执行切入点 joinPoint.proceed();
        System.out.println("环绕通知之后");
    } // 类似于 finally,保证一定会执行 @After(EXECUTION) public void after() {
        System.out.println("最终通知");
    } @AfterThrowing(EXECUTION) public void afterThrowing() {
        System.out.println("异常通知");
    }

} //=================================== 控制层测试代码 =================================== @RestController public class TestController { @GetMapping("/test") public void test() { // 可以查看如果发生异常,“通知”的执行顺序是怎样的 // int i = 1/0; System.out.println("test 请求");
    }
}

发送一个 /test 请求,查看控制台打印结果,可以看到,各个通知的执行顺序:

## 没有异常发生的情况 环绕通知之前
前置通知
test 请求
后置通知
最终通知
环绕通知之后 ## 异常发生的情况 环绕通知之前
前置通知
异常通知
最终通知 2022-08-23 14:34:06.587 ERROR 22324 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero

可以看到,如果发生了异常,那么切入点发生异常后续的代码、后置通知的代码、环绕通知之后的代码不会运行,并且最终通知一定会运行。

至此,Spring AOP 在 Spring Boot 如何使用已经简单介绍完毕,接下来看看如何使用 Spring AOP 实现登录鉴权。

使用注解和 Spring AOP 实现登录鉴权

假设我们有这样一个场景,某个接口需要用户具有管理员权限才能访问,如果没有权限则抛出异常,交给全局统一异常处理。

我们可以使用拦截器完成,也可以使用自定义切面编程实现。

我们需要编写三个类:


代码如下:

//=================================== 自定义注解代码 =================================== @Retention(RetentionPolicy.RUNTIME) @Documented @Target(ElementType.METHOD) @Component public @interface PermissionRole { // role 字段声明接口需要哪种权限角色才能访问,假定我们有两种角色,普通用户,管理员(admin) String role() default "";

} //=================================== 切面代码 =================================== @Aspect @Component public class PermissionRoleAspect { // 声明一个切入点,即标注了 @PermissionRole 注解的方法 @Pointcut("@annotation(com.example.aopdemo.annotation.PermissionRole)") public void check() {
    } // 声明切入点,这里 check() 主要是让我们获得“方法的信息”, //@annotation(permissionRole) 主要是让我们获得注解的信息,下面方法参数才能获取到 @PermissionRole 注解的实例信息 @Before("check() && @annotation(permissionRole)") public void before(JoinPoint joinPoint, PermissionRole permissionRole) throws Exception { // 可以在这里获取 token,检验用户是否登录,再执行后续代码 // 获取 @PermissionRole 中 role 字段的值 String role = permissionRole.role(); // 这里仅是为了方便测试,获取切入点的方法参数中携带过来信息,直接判断是否具有权限 // 实际过程中,我们应该根据 token 得到用户信息,在根据用户信息查询数据库该用户的权限,进行判断 for (Object arg : joinPoint.getArgs()) { if (arg instanceof String && arg.equals(role)) {
                System.out.println("权限验证通过"); return;
            }
        } throw new Exception("当前登录用户没有操作权限");
    }
} //=================================== 控制层测试代码 =================================== @RestController public class TestController { // 使用了自定义注解,当权限角色是 “admin” 时才能访问该接口 @PermissionRole(role = "admin") @GetMapping("/permission") public String roleApi(String token) {
        System.out.println("token = " + token); return "请求通过!";
    }

}

发送请求进行测试

## 发送请求 http://localhost:8080/permission ,不携带数据,即没有权限的情况下 2022-08-23 15:02:26.058 ERROR 14404 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.reflect.UndeclaredThrowableException] with root cause

java.lang.Exception: 当前登录用户没有操作权限 ## 发送请求 http://localhost:8080/permission?token=admin, 携带数据,有权限的情况下 权限验证通过
token = admin

总结

在 Spring Boot 使用 Spring AOP 时,我们需要引入一个 spring-boot-starter-aop ,就可以进行切面编程。

我们需要了解几个常用注解的用法:

  • @Aspect
  • @Pointcut
  • @Before
  • @Around
  • @AfterReturning
  • @AfterThrowing
  • @After

在声明切入点的时候,我们可以使用切入点表达式声明切入点。

此外,如果有多个切面,可以在切面类上使用注解 @Order 声明优先级,值越小优先级越高,越先执行。

#java#
全部评论
学到了,楼主厉害,感谢分享
点赞 回复 分享
发布于 2022-08-25 18:55 陕西

相关推荐

11-09 14:54
已编辑
华南农业大学 产品经理
大拿老师:这个简历,连手机号码和照片都没打码,那为什么关键要素求职职位就不写呢? 从上往下看,都没看出自己到底是产品经理的简历,还是电子硬件的简历? 这是一个大问题,当然,更大的问题是实习经历的描述是不对的 不要只是去写实习流程,陈平,怎么去开会?怎么去讨论? 面试问的是你的产品功能点,是怎么设计的?也就是要写项目的亮点,有什么功能?这个功能有什么难处?怎么去解决的? 实习流程大家都一样,没什么优势,也没有提问点,没有提问,你就不得分 另外,你要明确你投的是什么职位,如果投的是产品职位,你的项目经历写的全都是跟产品无关的,那你的简历就没用 你的面试官必然是一个资深的产品经理,他不会去问那些计算机类的编程项目 所以这种四不像的简历,在校招是大忌
点赞 评论 收藏
分享
三年之期已到我的offer快到碗里来:9硕都比不上9本
点赞 评论 收藏
分享
3 9 评论
分享
牛客网
牛客企业服务