AOP与注解的那些事儿~

  • 前言
  • 什么是AOP?
  • AOP的相关概念(面试常客)
  • Spring Boot 如何整合AOP自定义一个注解?
  • 使用拦截器如何自定义注解?
  • 内部调用导致AOP注解失效
  • 总结

注解相信大家都用过,尤其是 Spring Boot 这个框架,比如 @Controller

这篇文章就来介绍下 Spring Boot 中如何自定义一个注解,顺带介绍一下 Spring BootAOP如何整合。

什么是AOP?

AOP即是面向切面,是 Spring的核心功能之一,主要的目的即是针对业务处理过程中的横向拓展,以达到低耦合的效果。

举个栗子,项目中有记录操作日志的需求、或者流程变更是记录变更履历,无非就是插表操作,很简单的一个 save操作,都是一些记录日志或者其他辅助性的代码。一遍又一遍的重写和调用。不仅浪费了时间,又将项目变得更加的冗余,实在得不偿失。

此时 AOP的就该出场了,能够在不改变原逻辑的基础上实现相关功能。

AOP的相关概念(面试常客)

要理解 Spring Boot整合 Aop的实现,就必须先对面向切面实现的一些 Aop的概念有所了解,不然也是云里雾里。

「 切面(Aspect)」:一个关注点的模块化。以注解 @Aspect的形式放在类上方,声明一个切面。

「 连接点(Joinpoint)」:在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候都可以是连接点。

「 通知(Advice)」:通知增强,需要完成的工作叫做通知,就是你写的业务逻辑中需要比如事务、日志等先定义好,然后需要的地方再去用。增强包括如下五个方面:

「 切点(Pointcut)」:其实就是筛选出的连接点,匹配连接点的断言,一个类中的所有方法都是连接点,但又不全需要,会筛选出某些作为连接点做为切点。

「 引入(Introduction)」:在不改变一个现有类代码的情况下,为该类添加属性和方法,可以在无需修改现有类的前提下,让它们具有新的行为和状态。其实就是把切面(也就是新方法属性:通知定义的)用到目标类中去。

「 目标对象(Target Object)」:被一个或者多个切面所通知的对象。也被称做被通知( adviced)对象。既然 Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理( proxied)对象。

「AOP代理(AOP Proxy)」AOP框架创建的对象,用来实现切面契约(例如通知方法执行等等)。在 Spring中, AOP代理可以是 JDK动态代理或者 CGLIB代理。

「 织入(Weaving)」:把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用 AspectJ编译器),类加载时和运行时完成。 Spring和其他纯 Java AOP框架一样,在运行时完成织入。

Spring Boot 如何整合AOP自定义一个注解?

在实际开发中对于横向公共的逻辑需要抽取出来,这时候就需要使用 AOP,比如日志的记录、权限的验证等等,这些功能都可以用注解轻松的完成。

下面介绍如何在 Spring Boot使用 AOP定义一个注解。

AOP整合 Spring Boot有一个 starter,只需要添加依赖即可,如下:

<br>&#xA0;&#xA0;&#xA0;<span class="hljs-tag"><<span class="hljs-name">dependency</span>></span><br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-tag"><<span class="hljs-name">groupId</span>></span>org.springframework.boot<span class="hljs-tag">groupId</span>><br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-tag"><<span class="hljs-name">artifactId</span>></span>spring-boot-starter-aop<span class="hljs-tag">artifactId</span>><br>&#xA0;&#xA0;<span class="hljs-tag">dependency</span>>

在配置类上标注 @EnableAspectJAutoProxy注解即可开启 AOP,这个注解有什么用呢,源码如下:

(ElementType.TYPE)<br>(RetentionPolicy.RUNTIME)<br><br>(AspectJAutoProxyRegistrar<span class="hljs-class">.<span class="hljs-keyword">class</span>)<br><span class="hljs-title">public</span>&#xA0;@<span class="hljs-title">interface</span>&#xA0;<span class="hljs-title">EnableAspectJAutoProxy</span>&#xA0;</span>{}

最重要的是如下一行代码:

(AspectJAutoProxyRegistrar<span class="hljs-class">.<span class="hljs-keyword">class</span>)<br></span>

@Import这个注解很熟悉了吧,快速注入一个类,这里是注入一个 AnnotationAwareAspectJAutoProxyCreator

就以日志处理为例子,定义一个日志处理的注解,如下:

(ElementType.METHOD)<br>(RetentionPolicy.RUNTIME)<br><br><span class="hljs-keyword">public</span>&#xA0;&#xA0;SysLog&#xA0;{<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-function">String&#xA0;<span class="hljs-title">value</span><span class="hljs-params">()</span>&#xA0;<span class="hljs-keyword">default</span>&#xA0;""</span>;<br>}

一个切面的满足条件如下:

定义的日志切面如下:

<br><br>(Ordered.HIGHEST_PRECEDENCE)<br><span class="hljs-keyword">public</span>&#xA0;<span class="hljs-class"><span class="hljs-keyword">class</span>&#xA0;<span class="hljs-title">SysLogAspect</span>&#xA0;</span>{<br>}

@Order指定了切面执行的优先级,假如有多个切面,肯定是要有先后的执行顺序,这样才能保证逻辑性。

这里需要拦截的肯定是 @SysLog这个注解,只要方法上标注了该注解都将会被拦截,表达式如下:

(<span class="hljs-string">"@annotation(com.example.annotation_demo.annotation.SysLog)"</span>)<br><span class="hljs-function"><span class="hljs-keyword">public</span>&#xA0;<span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">pointCut</span><span class="hljs-params">()</span>&#xA0;</span>{}

既然是日志记录,肯定是在方法执行前,执行后都需要记录,因此需要定义一个环绕通知,如下:

&#xA0;&#xA0;(<span class="hljs-string">"pointCut()"</span>)<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-function"><span class="hljs-keyword">public</span>&#xA0;Object&#xA0;<span class="hljs-title">around</span><span class="hljs-params">(ProceedingJoinPoint&#xA0;point)</span>&#xA0;<span class="hljs-keyword">throws</span>&#xA0;Throwable&#xA0;</span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">long</span>&#xA0;beginTime&#xA0;=&#xA0;System.currentTimeMillis();<br><br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;Object&#xA0;result&#xA0;=&#xA0;point.proceed();<br><br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;saveLog(point,beginTime);<br><br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">return</span>&#xA0;result;<br>&#xA0;&#xA0;&#xA0;&#xA0;}

以上配置完成后即可使用,只需要在需要的方法上标注 @SysLog注解即可,如下:

<br>(<span class="hljs-string">"/add"</span>)<br><span class="hljs-function"><span class="hljs-keyword">public</span>&#xA0;String&#xA0;<span class="hljs-title">add</span><span class="hljs-params">()</span></span>{<br>&#xA0;&#xA0;<span class="hljs-keyword">return</span>&#xA0;<span class="hljs-string">""</span>;<br>}

使用拦截器如何自定义注解?

使用 AOP自定义的注解在每个方法上都会被拦截验证,首先效率上就不高。

然而拦截器是在每个 Controller方法执行之前进行拦截,其他的方法都不会生效,比如 service方法。

比如权限的验证、防止瞬间重复点击等等需求就适合使用拦截器自定义的注解。

就以防止瞬间重复点击的例子来创建一个注解,如下:

({ElementType.METHOD,&#xA0;ElementType.TYPE})<br>(RetentionPolicy.RUNTIME)<br><span class="hljs-keyword">public</span>&#xA0;&#xA0;RepeatSubmit&#xA0;{<br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-function"><span class="hljs-keyword">long</span>&#xA0;<span class="hljs-title">seconds</span><span class="hljs-params">()</span>&#xA0;<span class="hljs-keyword">default</span>&#xA0;5</span>;<br>}

需要在请求执行之前完成验证,逻辑很简单,就是判断方法上有没有标注 @RepeatSubmit注解,代码如下:

<br><br><span class="hljs-keyword">public</span>&#xA0;<span class="hljs-class"><span class="hljs-keyword">class</span>&#xA0;<span class="hljs-title">RepeatSubmitInterceptor</span>&#xA0;<span class="hljs-keyword">implements</span>&#xA0;<span class="hljs-title">HandlerInterceptor</span>&#xA0;</span>{<br><br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">private</span>&#xA0;StringRedisTemplate&#xA0;stringRedisTemplate;<br><br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-function"><span class="hljs-keyword">public</span>&#xA0;<span class="hljs-keyword">boolean</span>&#xA0;<span class="hljs-title">preHandle</span><span class="hljs-params">(HttpServletRequest&#xA0;request,&#xA0;HttpServletResponse&#xA0;response,&#xA0;Object&#xA0;handler)</span>&#xA0;<span class="hljs-keyword">throws</span>&#xA0;Exception&#xA0;</span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;(handler&#xA0;<span class="hljs-keyword">instanceof</span>&#xA0;HandlerMethod){<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;HandlerMethod&#xA0;handlerMethod=(HandlerMethod)handler;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;RepeatSubmit&#xA0;repeatSubmit&#xA0;=&#xA0;AnnotationUtils.findAnnotation(handlerMethod.getMethod(),RepeatSubmit<span class="hljs-class">.<span class="hljs-keyword">class</span>)</span>;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;(Objects.isNull(repeatSubmit))<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">return</span>&#xA0;<span class="hljs-keyword">true</span>;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;String&#xA0;flag=<span class="hljs-string">""</span>;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;Boolean&#xA0;ifAbsent&#xA0;=&#xA0;stringRedisTemplate.opsForValue().setIfAbsent(flag,&#xA0;<span class="hljs-string">""</span>,&#xA0;repeatSubmit.seconds(),&#xA0;TimeUnit.SECONDS);<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;(ifAbsent!=<span class="hljs-keyword">null</span>&&!ifAbsent)<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">throw</span>&#xA0;<span class="hljs-keyword">new</span>&#xA0;RepeatSubmitException();<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;}<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">return</span>&#xA0;<span class="hljs-keyword">true</span>;<br>&#xA0;&#xA0;&#xA0;&#xA0;}<br>}

在需要拦截方法上添加 @RepeatSubmit注解即可,如下:

&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;(<span class="hljs-string">"/add"</span>)<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-function"><span class="hljs-keyword">public</span>&#xA0;String&#xA0;<span class="hljs-title">add</span><span class="hljs-params">()</span></span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">return</span>&#xA0;<span class="hljs-string">""</span>;<br>&#xA0;&#xA0;&#xA0;&#xA0;}

内部调用导致AOP注解失效

这个问题在事务中也是经常被忽略的问题,网上很多人说是 AOPBug,其实在我看来这真不是一个 BUG,并且也是有办法解决的。

先来看一下失效的案例,如下:

<span class="hljs-keyword">public</span>&#xA0;<span class="hljs-class"><span class="hljs-keyword">class</span>&#xA0;<span class="hljs-title">ArticleServiceImpl</span></span>{<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<span class="hljs-function"><span class="hljs-keyword">public</span>&#xA0;<span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">A</span><span class="hljs-params">()</span></span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;......<br>&#xA0;&#xA0;}<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<span class="hljs-function"><span class="hljs-keyword">public</span>&#xA0;<span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">B</span><span class="hljs-params">()</span></span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">this</span>.A();<br>&#xA0;&#xA0;}<br>}

在上述的代码中,如果执行方法 B,则 @SysLog注解将会失效。

AOP使用的是动态代理的机制,它会给类生成一个代理类,事务的相关操作都在代理类上完成。内部方式使用 this调用方式时,使用的是实例调用,并没有通过代理类调用方法,所以会导致事务失效。

其实解决方法有很多,下面将会一一介绍。

在类内部通过 @Autowired将本身 bean引入,然后通过调用自身 bean,从而实现使用 AOP代理操作。代码如下:

<span class="hljs-keyword">public</span>&#xA0;<span class="hljs-class"><span class="hljs-keyword">class</span>&#xA0;<span class="hljs-title">ArticleServiceImpl</span></span>{<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<span class="hljs-keyword">private</span>&#xA0;ArticleService&#xA0;articleService;<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<span class="hljs-function"><span class="hljs-keyword">public</span>&#xA0;<span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">A</span><span class="hljs-params">()</span></span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;......<br>&#xA0;&#xA0;}<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<span class="hljs-function"><span class="hljs-keyword">public</span>&#xA0;<span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">B</span><span class="hljs-params">()</span></span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;articleService.A();<br>&#xA0;&#xA0;}<br>}

通过 ApplicationContext获取 bean,通过 bean调用内部方法,就使用了 bean的代理类。

需要先创建一个 ApplicationContext的工具类获取 ApplicationContext,然后才能调用 getBean()方法,代码如下:

<span class="hljs-keyword">public</span>&#xA0;<span class="hljs-class"><span class="hljs-keyword">class</span>&#xA0;<span class="hljs-title">ArticleServiceImpl</span></span>{<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<span class="hljs-function"><span class="hljs-keyword">public</span>&#xA0;<span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">A</span><span class="hljs-params">()</span></span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;......<br>&#xA0;&#xA0;}<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<span class="hljs-function"><span class="hljs-keyword">public</span>&#xA0;<span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">B</span><span class="hljs-params">()</span></span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;ApplicationContextUtils.getApplicationContext().getBean(ArticleService<span class="hljs-class">.<span class="hljs-keyword">class</span>).<span class="hljs-title">A</span>()</span>;<br>&#xA0;&#xA0;}<br>}

此种方法需要设置 @EnableAspectJAutoProxy中的 exposeProxytrue

使用 AopContext获取当前的代理对象,代码如下:

<span class="hljs-keyword">public</span>&#xA0;<span class="hljs-class"><span class="hljs-keyword">class</span>&#xA0;<span class="hljs-title">ArticleServiceImpl</span></span>{<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<span class="hljs-function"><span class="hljs-keyword">public</span>&#xA0;<span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">A</span><span class="hljs-params">()</span></span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;......<br>&#xA0;&#xA0;}<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;<span class="hljs-function"><span class="hljs-keyword">public</span>&#xA0;<span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">B</span><span class="hljs-params">()</span></span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;((ArticleService)AopContext.currentProxy()).A();<br>&#xA0;&#xA0;}<br>}

这篇文章介绍了 AOP的相关概念、 AOP实现自定义注解以及拦截器实现自定义注解,都是日常开发中必备的知识点,希望这篇文章对各位有所帮助。

源码已经上传,回复关键词 AOP&#x6CE8;&#x89E3;获取。

最后,别忘了点赞哦!!!

另外作者的第一本 PDF书籍已经整理好了,由浅入深的详细介绍了Mybatis基础以及底层源码,有需要的朋友回复关键词 「Mybatis进阶」即可获取,目录如下:

Original: https://www.cnblogs.com/Chenjiabing/p/13984651.html
Author: 爱撒谎的男孩
Title: AOP与注解的那些事儿~

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/712753/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球