ToB企服应用市场:ToB评测及商务社交产业平台

标题: Spring AOP基础、快速入门 [打印本页]

作者: 我爱普洱茶    时间: 2024-12-10 08:18
标题: Spring AOP基础、快速入门
介绍

AOP,面向切面编程,作为面向对象的一种增补,将公共逻辑(事件管理、日志、缓存、权限控制、限流等)封装成切面,跟业务代码进行分离,可以减少系统的重复代码降低模块之间的耦合度。切面就是那些与业务无关,但全部业务模块都会调用的公共逻辑。
先看一个例子:怎样给如下UserServiceImpl中全部方法添加进入方法的日志,
  1. public class UserServiceImpl implements IUserService {
  2.     @Override
  3.     public List<User> findUserList() {
  4.         System.out.println("execute method: findUserList");
  5.         return Collections.singletonList(new User("seven", 18));
  6.     }
  7.     @Override
  8.     public void addUser() {
  9.         System.out.println("execute method: addUser");
  10.         // do something
  11.     }
  12. }
复制代码
将记录日志功能解耦为日志切面,它的目标是解耦。进而引出AOP的理念:就是将分散在各个业务逻辑代码中雷同的代码通过横向切割的方式抽取到一个独立的模块中!

OOP面向对象编程,针对业务处置惩罚过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。
AOP则是针对业务处置惩罚过程中的切面进行提取,它所面对的是处置惩罚过程的某个步调或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。这两种设计思想在目标上有着本质的差异。

AOP相干术语

起重要知道,aop不是spring所特有的,同样的,这些术语也不是spring所特有的。是由AOP联盟定义的

execution表达式格式:
  1. execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
复制代码
例如:
  1. execution(* com.seven.springframeworkaopannojdk.service.*.*(..))
复制代码
Spring AOP和AspectJ的关系

AspectJ是一个java实现的AOP框架,它可以或许对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(固然必要特殊的编译器)。可以如许说AspectJ是目前实现AOP框架中最成熟,功能最丰富的语言,更幸运的是,AspectJ与java程序完全兼容,险些是无缝关联,因此对于有java编程基础的工程师,上手和使用都非常容易。
下表总结了 Spring AOP 和 AspectJ 之间的关键区别:
Spring AOPAspectJ在纯 Java 中实现使用 Java 编程语言的扩展实现不必要单独的编译过程除非设置 LTW,否则必要 AspectJ 编译器 (ajc)只能使用运行时织入运行时织入不可用。支持编译时、编译后和加载时织入功能不强 - 仅支持方法级编织更强大 - 可以编织字段、方法、构造函数、静态初始值设定项、终极类/方法等......。只能在由 Spring 容器管理的 bean 上实现可以在全部域对象上实现仅支持方法实行切入点支持全部切入点代理是由目标对象创建的, 而且切面应用在这些代理上在实行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入比 AspectJ 慢多了更好的性能易于学习和应用相对于 Spring AOP 来说更复杂AOP的实现原理

AOP有两种实现方式:静态代理和动态代理。
静态代理

静态代理分为:编译时织入(特殊编译器实现)、类加载时织入(特殊的类加载器实现)。
代理类在编译阶段生成,在编译阶段将增强织入Java字节码中,也称编译时增强。AspectJ使用的是静态代理
缺点:代理对象必要与目标对象实现一样的接口,而且实现接口的方法,会有冗余代码。同时,一旦接口增加方法,目标对象与代理对象都要维护。
动态代理

动态代理:代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类
Spring的AOP实现原理

而Spring的AOP的实现就是通过动态代理实现的。
如果为Spring的某个bean设置了切面,那么Spring在创建这个bean的时候,现实上创建的是这个bean的一个代理对象,后续对bean中方法的调用,现实上调用的是代理类重写的代理方法。而Spring的AOP使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理。
那么什么时候采用哪种动态代理呢?
AOP的设置方式

基于XML

Spring提供了使用"aop"定名空间来定义一个切面,我们来看个例子
  1. public class AopDemoServiceImpl {
  2.     public void doMethod1() {
  3.         System.out.println("AopDemoServiceImpl.doMethod1()");
  4.     }
  5.     public String doMethod2() {
  6.         System.out.println("AopDemoServiceImpl.doMethod2()");
  7.         return "hello world";
  8.     }
  9.     public String doMethod3() throws Exception {
  10.         System.out.println("AopDemoServiceImpl.doMethod3()");
  11.         throw new Exception("some exception");
  12.     }
  13. }
复制代码
  1. public class LogAspect {
  2.     public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
  3.         System.out.println("-----------------------");
  4.         System.out.println("环绕通知: 进入方法");
  5.         Object o = pjp.proceed();
  6.         System.out.println("环绕通知: 退出方法");
  7.         return o;
  8.     }
  9.     public void doBefore() {
  10.         System.out.println("前置通知");
  11.     }
  12.     public void doAfterReturning(String result) {
  13.         System.out.println("后置通知, 返回值: " + result);
  14.     }
  15.     public void doAfterThrowing(Exception e) {
  16.         System.out.println("异常通知, 异常: " + e.getMessage());
  17.     }
  18.     public void doAfter() {
  19.         System.out.println("最终通知");
  20.     }
  21. }
复制代码
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4.        xmlns:aop="http://www.springframework.org/schema/aop"
  5.        xmlns:context="http://www.springframework.org/schema/context"
  6.        xsi:schemaLocation="http://www.springframework.org/schema/beans
  7. http://www.springframework.org/schema/beans/spring-beans.xsd
  8. http://www.springframework.org/schema/aop
  9. http://www.springframework.org/schema/aop/spring-aop.xsd
  10. http://www.springframework.org/schema/context
  11. http://www.springframework.org/schema/context/spring-context.xsd
  12. ">
  13.     <context:component-scan base-package="com.seven.springframeworkaopxml" />
  14.     <aop:aspectj-autoproxy/>
  15.    
  16.     <bean id="demoService" >
  17.         
  18.     </bean>
  19.    
  20.     <bean id="logAspect" >
  21.         
  22.     </bean>
  23.     <aop:config>
  24.         
  25.         <aop:aspect ref="logAspect">
  26.             
  27.             <aop:pointcut id="pointCutMethod" expression="execution(* com.seven.springframeworkaopxml.service.*.*(..))"/>
  28.             
  29.             <aop:around method="doAround" pointcut-ref="pointCutMethod"/>
  30.             
  31.             <aop:before method="doBefore" pointcut-ref="pointCutMethod"/>
  32.             
  33.             <aop:after-returning method="doAfterReturning" pointcut-ref="pointCutMethod" returning="result"/>
  34.             
  35.             <aop:after-throwing method="doAfterThrowing" pointcut-ref="pointCutMethod" throwing="e"/>
  36.             
  37.             <aop:after method="doAfter" pointcut-ref="pointCutMethod"/>
  38.         </aop:aspect>
  39.     </aop:config>
  40. </beans>
复制代码
  1. public static void main(String[] args) {
  2.     // create and configure beans
  3.     ApplicationContext context = new ClassPathXmlApplicationContext("aspects.xml");
  4.     // retrieve configured instance
  5.     AopDemoServiceImpl service = context.getBean("demoService", AopDemoServiceImpl.class);
  6.     // use configured instance
  7.     service.doMethod1();
  8.     service.doMethod2();
  9.     try {
  10.         service.doMethod3();
  11.     } catch (Exception e) {
  12.         // e.printStackTrace();
  13.     }
  14. }
复制代码
基于AspectJ注解(直接写表达式)

基于XML的声明式AspectJ存在一些不足,必要在Spring设置文件设置大量的代码信息,为相识决这个问题,Spring 使用了@AspectJ框架为AOP的实现提供了一套注解。
注解名称解释@Aspect用来定义一个切面。@pointcut用于定义切入点表达式。在使用时还必要定义一个包含名字和任意参数的方法签名来表示切入点名称,这个方法签名就是一个返回值为void,且方法体为空的普通方法。@Before用于定义前置关照,相当于BeforeAdvice。在使用时,通常必要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)。@AfterReturning用于定义后置关照,相当于AfterReturningAdvice。在使用时可以指定pointcut / value和returning属性,此中pointcut / value这两个属性的作用一样,都用于指定切入点表达式。@Around用于定义环绕关照,相当于MethodInterceptor。在使用时必要指定一个value属性,该属性用于指定该关照被植入的切入点。@After-Throwing用于定义异常关照来处置惩罚程序中未处置惩罚的异常,相当于ThrowAdvice。在使用时可指定pointcut / value和throwing属性。此中pointcut/value用于指定切入点表达式,而throwing属性值用于指定-一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常。@After用于定义终极final 关照,不管是否异常,该关照都会实行。使用时必要指定一个value属性,该属性用于指定该关照被植入的切入点。@DeclareParents用于定义引介关照,相当于IntroductionInterceptor (不要求掌握)。基于JDK动态代理

基于JDK动态代理例子源码点这里
  1. public interface IJdkProxyService {
  2.     void doMethod1();
  3.     String doMethod2();
  4.     String doMethod3() throws Exception;
  5. }
复制代码
  1. @Service
  2. public class JdkProxyDemoServiceImpl implements IJdkProxyService {
  3.     @Override
  4.     public void doMethod1() {
  5.         System.out.println("JdkProxyServiceImpl.doMethod1()");
  6.     }
  7.     @Override
  8.     public String doMethod2() {
  9.         System.out.println("JdkProxyServiceImpl.doMethod2()");
  10.         return "hello world";
  11.     }
  12.     @Override
  13.     public String doMethod3() throws Exception {
  14.         System.out.println("JdkProxyServiceImpl.doMethod3()");
  15.         throw new Exception("some exception");
  16.     }
  17. }
复制代码
  1. @EnableAspectJAutoProxy
  2. @Component
  3. @Aspect
  4. public class LogAspect {
  5.     /**
  6.      * define point cut.
  7.      */
  8.     @Pointcut("execution(* com.seven.springframeworkaopannojdk.service.*.*(..))")
  9.     private void pointCutMethod() {
  10.     }
  11.     /**
  12.      * 环绕通知.
  13.      *
  14.      * @param pjp pjp
  15.      * @return obj
  16.      * @throws Throwable exception
  17.      */
  18.     @Around("pointCutMethod()")
  19.     public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
  20.         System.out.println("-----------------------");
  21.         System.out.println("环绕通知: 进入方法");
  22.         Object o = pjp.proceed();
  23.         System.out.println("环绕通知: 退出方法");
  24.         return o;
  25.     }
  26.     /**
  27.      * 前置通知.
  28.      */
  29.     @Before("pointCutMethod()")
  30.     public void doBefore() {
  31.         System.out.println("前置通知");
  32.     }
  33.     /**
  34.      * 后置通知.
  35.      *
  36.      * @param result return val
  37.      */
  38.     @AfterReturning(pointcut = "pointCutMethod()", returning = "result")
  39.     public void doAfterReturning(String result) {
  40.         System.out.println("后置通知, 返回值: " + result);
  41.     }
  42.     /**
  43.      * 异常通知.
  44.      *
  45.      * @param e exception
  46.      */
  47.     @AfterThrowing(pointcut = "pointCutMethod()", throwing = "e")
  48.     public void doAfterThrowing(Exception e) {
  49.         System.out.println("异常通知, 异常: " + e.getMessage());
  50.     }
  51.     /**
  52.      * 最终通知.
  53.      */
  54.     @After("pointCutMethod()")
  55.     public void doAfter() {
  56.         System.out.println("最终通知");
  57.     }
  58. }
复制代码
  1. public class App {
  2.     public static void main(String[] args) {
  3.         // create and configure beans
  4.         ApplicationContext context = new AnnotationConfigApplicationContext("com.seven.springframeworkaopannojdk");
  5.         // retrieve configured instance
  6.         IJdkProxyService service = context.getBean(IJdkProxyService.class);
  7.         // use configured instance
  8.         service.doMethod1();
  9.         service.doMethod2();
  10.         try {
  11.             service.doMethod3();
  12.         } catch (Exception e) {
  13.             // e.printStackTrace();
  14.         }
  15.     }
  16. }
复制代码
非接口使用Cglib代理

基于Cglib代理例子源码点这里
  1. @Service
  2. public class CglibProxyDemoServiceImpl {
  3.     public void doMethod1() {
  4.         System.out.println("CglibProxyDemoServiceImpl.doMethod1()");
  5.     }
  6.     public String doMethod2() {
  7.         System.out.println("CglibProxyDemoServiceImpl.doMethod2()");
  8.         return "hello world";
  9.     }
  10.     public String doMethod3() throws Exception {
  11.         System.out.println("CglibProxyDemoServiceImpl.doMethod3()");
  12.         throw new Exception("some exception");
  13.     }
  14. }
复制代码
和上面雷同
  1. public class App {
  2.     public static void main(String[] args) {
  3.         // create and configure beans
  4.         ApplicationContext context = new AnnotationConfigApplicationContext("com.seven.springframeworkaopannocglib");
  5.         // cglib proxy demo
  6.         CglibProxyDemoServiceImpl service = context.getBean(CglibProxyDemoServiceImpl.class);
  7.         service.doMethod1();
  8.         service.doMethod2();
  9.         try {
  10.             service.doMethod3();
  11.         } catch (Exception e) {
  12.             // e.printStackTrace();
  13.         }
  14.     }
  15. }
复制代码
使用注解装配AOP

上面使用AspectJ的注解,并共同一个复杂的execution(* com.seven.springframeworkaopannojdk.service.*.*(..)) 语法来定义应该怎样装配AOP。另有另一种方式,则是使用注解来装配AOP,这两者一般存在与不同的应用场景中:
  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface LogAspectAnno {
  4. }
复制代码
  1. @EnableAspectJAutoProxy
  2. @Component
  3. @Aspect
  4. public class LogAspect {
  5.     @Around("@annotation(logaspectanno)") //注意,括号里为logaspectanno,而不是LogAspectAnno
  6.     public Object doAround(ProceedingJoinPoint pjp, LogAspectAnno logaspectanno) throws Throwable {
  7.         System.out.println("-----------------------");
  8.         System.out.println("环绕通知: 进入方法");
  9.         Object o = pjp.proceed();
  10.         System.out.println("环绕通知: 退出方法");
  11.         return o;
  12.     }
  13.    
  14. }
复制代码
  1. @Service
  2. public class CglibProxyDemoServiceImpl {
  3.     @LogAspectAnno()
  4.     public void doMethod1() {
  5.         System.out.println("CglibProxyDemoServiceImpl.doMethod1()");
  6.     }
  7.     public String doMethod2() {
  8.         System.out.println("CglibProxyDemoServiceImpl.doMethod2()");
  9.         return "hello world";
  10.     }
  11. }
  12. @Service
  13. public class JdkProxyDemoServiceImpl implements IJdkProxyService {
  14.     @LogAspectAnno
  15.     @Override
  16.     public void doMethod1() {
  17.         System.out.println("JdkProxyServiceImpl.doMethod1()");
  18.     }
  19.     @Override
  20.     public String doMethod2() {
  21.         System.out.println("JdkProxyServiceImpl.doMethod2()");
  22.         return "hello world";
  23.     }
  24. }
复制代码
  1. // create and configure beans
  2. ApplicationContext context = new AnnotationConfigApplicationContext("com.seven.springframeworkaopannotation");
  3. // cglib proxy demo
  4. CglibProxyDemoServiceImpl service1 = context.getBean(CglibProxyDemoServiceImpl.class);
  5. service1.doMethod1();
  6. service1.doMethod2();
  7. IJdkProxyService service2 = context.getBean(IJdkProxyService.class);
  8. service2.doMethod1();
  9. service2.doMethod2();
复制代码
  1. -----------------------
  2. 环绕通知: 进入方法
  3. CglibProxyDemoServiceImpl.doMethod1()
  4. 环绕通知: 退出方法
  5. CglibProxyDemoServiceImpl.doMethod2()
  6. -----------------------
  7. 环绕通知: 进入方法
  8. JdkProxyServiceImpl.doMethod1()
  9. 环绕通知: 退出方法
  10. JdkProxyServiceImpl.doMethod2()
复制代码
可以看到,只有doMethod1方法被增强了,doMethod2没有被增强,就是因为@LogAspectAnno 只注解了 doMethod1() 方法,从而实现更精细化的控制,是业务感知到这个方法是被增强了。
应用场景

我们知道AO可以或许将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事件处置惩罚、日志管理、权限控制等)封装起来,便于减少系统的重复代码降低模块间的耦合度提高系统可拓展性和可维护性
日志记录

使用 AOP 方式记录日志,只必要在 Controller 的方法上使用自定义 @Log 日志注解,就可以将用户操纵记录到数据库。
  1. @Log(description = "新增用户")
  2. @PostMapping(value = "/users")
  3. public ResponseEntity create(@Validated @RequestBody User resources){
  4.     checkLevel(resources);
  5.     return new ResponseEntity(userService.create(resources),HttpStatus.CREATED);
  6. }
复制代码
AOP 切面类 LogAspect用来拦截带有 @Log 注解的方法并处置惩罚:
  1. @Aspect
  2. @Component
  3. public class LogAspect {
  4.     private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
  5.     // 定义切点,拦截带有 @Log 注解的方法
  6.     @Pointcut("@annotation(com.example.annotation.Log)") // 这里需要根据你的实际包名修改
  7.     public void logPointcut() {
  8.     }
  9.     // 环绕通知,用于记录日志
  10.     @Around("logPointcut()")
  11.     public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
  12.       //...
  13.     }
  14. }
复制代码
限流

使用 AOP 方式对接口进行限流,只必要在 Controller 的方法上使用自定义的 @RateLimit 限流注解即可。
  1. /**
  2. * 该接口 60 秒内最多只能访问 10 次,保存到 redis 的键名为 limit_test,
  3. */
  4. @RateLimit(key = "test", period = 60, count = 10, name = "testLimit", prefix = "limit")
  5. public int test() {
  6.      return ATOMIC_INTEGER.incrementAndGet();
  7. }
复制代码
AOP 切面类 RateLimitAspect用来拦截带有 @RateLimit 注解的方法并处置惩罚:
  1. @Slf4j
  2. @Aspect
  3. public class RateLimitAspect {
  4.       // 拦截所有带有 @RateLimit 注解的方法
  5.       @Around("@annotation(rateLimit)")
  6.     public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
  7.       //...
  8.     }
  9. }
复制代码
关于限流实现这里多说一句,这里并没有自己写 Redis Lua 限流脚本,而是使用 Redisson 中的 RRateLimiter 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。
权限控制

Spring Security 使用 AOP 进行方法拦截。在现实调用 update 方法之前,Spring 会检查当前用户的权限,只有用户权限满意对应的条件才华实行。
  1. @Log(description = "修改菜单")
  2. @PutMapping(value = "/menus")
  3. // 用户拥有 `admin`、`menu:edit` 权限中的任意一个就能能访问`update`方法
  4. @PreAuthorize("hasAnyRole('admin','menu:edit')")
  5. public ResponseEntity update(@Validated @RequestBody Menu resources){
  6.     //...
  7. }
复制代码
面试题专栏

Java面试题专栏已上线,欢迎访问。
那么可以私信我,我会尽我所能资助你。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4