Spring Boot实践——AOP实现
借鉴:http://www.cnblogs.com/xrq730/p/4919025.html
https://blog.csdn.net/zhaokejin521/article/details/50144753
http://www.importnew.com/24305.html
AOP 介绍
一、AOP
AOP(Aspect Oriented Programming),即面向切面编程,可以说是 OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP 引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过 OOP 允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在 OOP 设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP 技术恰恰相反,它利用一种称为 "横切" 的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为 "Aspect",即切面。所谓 "切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
使用 "横切" 技术,AOP 把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
AOP(Aspect Orient Programming),我们一般称为面向方面(切面)编程,作为面向对象的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理、日志、缓存等等。AOP 实现的关键在于 AOP 框架自动创建的 AOP 代理,AOP 代理主要分为静态代理和动态代理,静态代理的代表为 AspectJ;而动态代理则以 Spring AOP 为代表。
与 AspectJ 的静态代理不同,Spring AOP 使用的动态代理,所谓的动态代理就是说 AOP 框架不会去修改字节码,而是在内存中临时为方法生成一个 AOP 对象,这个 AOP 对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP 中的动态代理主要有两种方式,JDK 动态代理和 CGLIB 动态代理。JDK 动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK 动态代理的核心是InvocationHandler
接口和Proxy
类。
如果目标类没有实现接口,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,是利用 asm 开源包,可以在运行时动态的生成某个类的子类。注意,CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为final
,那么它是无法使用 CGLIB 做动态代理的。
二、AOP 核心概念
1、横切关注点
对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点
2、切面(aspect)
类是对物体特征的抽象,切面就是对横切关注点的抽象
3、连接点(joinpoint)
被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring 中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
4、切入点(pointcut)
对连接点进行拦截的定义
5、通知(advice)
所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类
6、目标对象
代理的目标对象
7、织入(weave)
将切面应用到目标对象并导致代理对象创建的过程
8、引入(introduction)
在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
三、Spring 对 AOP 的支持
Spring 中 AOP 代理由 Spring 的 IOC 容器负责生成、管理,其依赖关系也由 IOC 容器负责管理。因此,AOP 代理可以直接使用容器中的其它 bean 实例作为目标,这种关系可由 IOC 容器的依赖注入提供。Spring 创建代理的规则为:
1、默认使用 Java 动态代理来创建 AOP 代理,这样就可以为任何接口实例创建代理了
2、当需要代理的类不是代理接口的时候,Spring 会切换为使用 CGLIB 代理,也可强制使用 CGLIB
AOP 编程其实是很简单的事情,纵观 AOP 编程,程序员只需要参与三个部分:
1、定义普通业务组件
2、定义切入点,一个切入点可能横切多个业务组件
3、定义增强处理,增强处理就是在 AOP 框架为普通业务组件织入的处理动作
所以进行 AOP 编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP 框架将自动生成 AOP 代理,即:代理对象的方法 = 增强处理 + 被代理对象的方法。
实现方式
Spring 除了支持 Schema 方式配置 AOP,还支持注解方式:使用 @AspectJ 风格的切面声明。
一、Aspectj 介绍
@AspectJ 作为通过 Java 5 注释注释的普通的 Java 类,它指的是声明 aspects 的一种风格。
AspectJ 是静态代理的增强,所谓的静态代理就是 AOP 框架会在编译阶段生成 AOP 代理类,因此也称为编译时增强。
AspectJ: 基于字节码操作 (Bytecode Manipulation),通过编织阶段 (Weaving Phase),对目标 Java 类型的字节码进行操作,将需要的 Advice 逻辑给编织进去,形成新的字节码。毕竟 JVM 执行的都是 Java 源代码编译后得到的字节码,所以 AspectJ 相当于在这个过程中做了一点手脚,让 Advice 能够参与进来。
而编织阶段可以有两个选择,分别是加载时编织 (也可以成为运行时编织) 和编译时编织
- 加载时编织 (Load-Time Weaving):顾名思义,这种编织方式是在 JVM 加载类的时候完成的。
- 编译时编织 (Compile-Time Weaving):需要使用 AspectJ 的编译器来替换 JDK 的编译器。
详情:AOP 的两种实现 -Spring AOP 以及 AspectJ
1、添加 spirng aop 支持和 AspectJ 依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.0.7.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.13</version> </dependency>
或
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> </parent><dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
</dependencies>
2、启用对 @AspectJ 的支持
Spring 默认不支持 @AspectJ 风格的切面声明,为了支持需要使用如下配置:
<!-- 自动扫描使用了 aspectj 注解的类 --> <aop:aspectj-autoproxy/>
或者在配置类上加注解
@Configuration
@ComponentScan("com.only.mate.springboot.aop")
@EnableAspectJAutoProxy// 开启 AspectJ 注解
public class CustomAopConfigurer {
}
3、声明切面
@Aspect @Component public class CustomLogAspect { }
或者
定一个普通类
public class CustomAuthorityAspect { }
在配置文件中定义一个 POJO
<bean id="customAuthorityAspect" class="com.only.mate.springboot.aop.CustomAuthorityAspect" />
然后在该切面中进行切入点及通知定义,接着往下看吧。
4、声明切入点
@AspectJ 风格的命名切入点使用 org.aspectj.lang.annotation 包下的 @Pointcut+ 方法(方法必须是返回 void 类型)实现。
@Pointcut(value="切入点表达式", argNames = "参数名列表") public void pointcutName(……) {}
value:指定切入点表达式;
argNames:指定命名切入点方法参数列表参数名字,可以有多个用“,”分隔,这些参数将传递给通知方法同名的参数,同时比如切入点表达式“args(param)”将匹配参数类型为命名切入点方法同名参数指定的参数类型。
pointcutName:切入点名字,可以使用该名字进行引用该切入点表达式。
案例:
@Pointcut(value="execution(* com.only.mate.springboot.controller.*.sayAdvisorBefore(..)) && args(param)", argNames = "param") public void pointCut(String param) {}
定义了一个切入点,名字为“pointCut”,该切入点将匹配目标方法的第一个参数类型为通知方法实现中参数名为“param”的参数类型。
5、声明通知
@AspectJ 风格的声明通知也支持 5 种通知类型:
A、前置通知:使用 org.aspectj.lang.annotation 包下的 @Before 注解声明。
@Before(value = "切入点表达式或命名切入点", argNames = "参数列表参数名")
value:指定切入点表达式或命名切入点。
argNames:与 Schema 方式配置中的同义。
B、后置返回通知:使用 org.aspectj.lang.annotation 包下的 @AfterReturning 注解声明。
@AfterReturning( value="切入点表达式或命名切入点", pointcut="切入点表达式或命名切入点", argNames="参数列表参数名", returning="返回值对应参数名")
value:指定切入点表达式或命名切入点。
pointcut:同样是指定切入点表达式或命名切入点,如果指定了将覆盖 value 属性指定的,pointcut 具有高优先级。
argNames:与 Schema 方式配置中的同义。
returning:与 Schema 方式配置中的同义。
C、后置异常通知:使用 org.aspectj.lang.annotation 包下的 @AfterThrowing 注解声明。
@AfterThrowing ( value="切入点表达式或命名切入点", pointcut="切入点表达式或命名切入点", argNames="参数列表参数名", throwing="异常对应参数名")
value:指定切入点表达式或命名切入点。
pointcut:同样是指定切入点表达式或命名切入点,如果指定了将覆盖 value 属性指定的,pointcut 具有高优先级。
argNames:与 Schema 方式配置中的同义。
throwing:与 Schema 方式配置中的同义。
D、后置最终通知:使用 org.aspectj.lang.annotation 包下的 @After 注解声明。
@After ( value="切入点表达式或命名切入点", argNames="参数列表参数名")
value:指定切入点表达式或命名切入点。
argNames:与 Schema 方式配置中的同义。
E、环绕通知:使用 org.aspectj.lang.annotation 包下的 @Around 注解声明。
@Around ( value="切入点表达式或命名切入点", argNames="参数列表参数名")
value:指定切入点表达式或命名切入点。
argNames:与 Schema 方式配置中的同义。
二、实践
1、Schema 方式配置 AOP
A、定一个切入点
/** * 自定义一个切入点 - 权限校验 * @ClassName: CustomAuthorityAspect * @Description: TODO * @author OnlyMate * @Date 2018 年 9 月 7 日 下午 2:24:24 * */ public class CustomAuthorityAspect { private Logger logger = LoggerFactory.getLogger(CustomLogAspect.class); /** * 加密 * @Title: encode * @Description: TODO * @Date 2018 年 9 月 7 日 下午 2:30:05 * @author OnlyMate */ public void encode() { logger.info("CustomAuthorityAspect ==> encode method: encode data");}</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 解密 * @Title: decode * @Description: TODO * @Date 2018年9月7日 下午2:30:11 * </span><span style="color: rgba(128, 128, 128, 1)">@author</span><span style="color: rgba(0, 128, 0, 1)"> OnlyMate </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> decode() { logger.info(</span>"CustomAuthorityAspect ==> decode method: decode data"<span style="color: rgba(0, 0, 0, 1)">); }
}
B、通过 Schema 方式配置 AOP
<bean id="customAuthorityAspect" class="com.only.mate.springboot.aop.CustomAuthorityAspect" /><aop:config proxy-target-class="false">
<!-- AOP 实现 -->
<aop:aspect id="customAuthority" ref="customAuthorityAspect">
<aop:pointcut id="addAllMethod" expression="execution(* com.only.mate.springboot.controller..(..))" />
<aop:before method="encode" pointcut-ref="addAllMethod" />
<aop:after method="decode" pointcut-ref="addAllMethod" />
</aop:aspect>
</aop:config>
前面说过 Spring 使用动态代理或是 CGLIB 生成代理是有规则的,高版本的 Spring 会自动选择是使用动态代理还是 CGLIB 生成代理内容,当然我们也可以强制使用 CGLIB 生成代理,那就是 <aop:config> 里面有一个 "proxy-target-class" 属性,这个属性值如果被设置为 true,那么基于类的代理将起作用,如果 proxy-target-class 被设置为 false 或者这个属性被省略,那么基于接口的代理将起作用
2、使用 @AspectJ 风格的切面声明
A、定一个切入点
/** * @Description: 自定义切面 * @ClassName: CustomLogAspect * @author OnlyMate * @Date 2018 年 9 月 10 日 下午 3:51:32 * */ @Aspect @Component public class CustomLogAspect { private Logger logger = LoggerFactory.getLogger(CustomLogAspect.class);</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * @Description: 定义切入点 * @Title: pointCut * </span><span style="color: rgba(128, 128, 128, 1)">@author</span><span style="color: rgba(0, 128, 0, 1)"> OnlyMate * @Date 2018年9月10日 下午3:52:17 </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">被注解CustomAopAnnotation表示的方法 </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">@Pointcut("@annotation(com.only.mate.springboot.annotation.CustomAopAnnotation")</span> @Pointcut("execution(public * com.only.mate.springboot.controller.*.*(..))"<span style="color: rgba(0, 0, 0, 1)">) </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> pointCut(){ } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * @Description: 定义前置通知 * @Title: before * </span><span style="color: rgba(128, 128, 128, 1)">@author</span><span style="color: rgba(0, 128, 0, 1)"> OnlyMate * @Date 2018年9月10日 下午3:52:23 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> joinPoint * </span><span style="color: rgba(128, 128, 128, 1)">@throws</span><span style="color: rgba(0, 128, 0, 1)"> Throwable </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> @Before(</span>"pointCut()"<span style="color: rgba(0, 0, 0, 1)">) </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> before(JoinPoint joinPoint) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Throwable { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 接收到请求,记录请求内容</span> logger.info("【注解:Before】------------------切面 before"<span style="color: rgba(0, 0, 0, 1)">); ServletRequestAttributes attributes </span>=<span style="color: rgba(0, 0, 0, 1)"> (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request </span>=<span style="color: rgba(0, 0, 0, 1)"> attributes.getRequest(); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 记录下请求内容</span> logger.info("【注解:Before】浏览器输入的网址=URL : " +<span style="color: rgba(0, 0, 0, 1)"> request.getRequestURL().toString()); logger.info(</span>"【注解:Before】HTTP_METHOD : " +<span style="color: rgba(0, 0, 0, 1)"> request.getMethod()); logger.info(</span>"【注解:Before】IP : " +<span style="color: rgba(0, 0, 0, 1)"> request.getRemoteAddr()); logger.info(</span>"【注解:Before】执行的业务方法名=CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." +<span style="color: rgba(0, 0, 0, 1)"> joinPoint.getSignature().getName()); logger.info(</span>"【注解:Before】业务方法获得的参数=ARGS : " +<span style="color: rgba(0, 0, 0, 1)"> Arrays.toString(joinPoint.getArgs())); } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * @Description: 后置返回通知 * @Title: afterReturning * </span><span style="color: rgba(128, 128, 128, 1)">@author</span><span style="color: rgba(0, 128, 0, 1)"> OnlyMate * @Date 2018年9月10日 下午3:52:30 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> ret * </span><span style="color: rgba(128, 128, 128, 1)">@throws</span><span style="color: rgba(0, 128, 0, 1)"> Throwable </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> @AfterReturning(returning </span>= "ret", pointcut = "pointCut()"<span style="color: rgba(0, 0, 0, 1)">) </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> afterReturning(Object ret) <span style="color: rgba(0, 0, 255, 1)">throws</span><span style="color: rgba(0, 0, 0, 1)"> Throwable { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 处理完请求,返回内容</span> logger.info("【注解:AfterReturning】这个会在切面最后的最后打印,方法的返回值 : " +<span style="color: rgba(0, 0, 0, 1)"> ret); } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * @Description: 后置异常通知 * @Title: afterThrowing * </span><span style="color: rgba(128, 128, 128, 1)">@author</span><span style="color: rgba(0, 128, 0, 1)"> OnlyMate * @Date 2018年9月10日 下午3:52:37 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> jp </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> @AfterThrowing(</span>"pointCut()"<span style="color: rgba(0, 0, 0, 1)">) </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> afterThrowing(JoinPoint jp){ logger.info(</span>"【注解:AfterThrowing】方法异常时执行....."<span style="color: rgba(0, 0, 0, 1)">); } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * @Description: 后置最终通知,final增强,不管是抛出异常或者正常退出都会执行 * @Title: after * </span><span style="color: rgba(128, 128, 128, 1)">@author</span><span style="color: rgba(0, 128, 0, 1)"> OnlyMate * @Date 2018年9月10日 下午3:52:48 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> jp </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> @After(</span>"pointCut()"<span style="color: rgba(0, 0, 0, 1)">) </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> after(JoinPoint jp){ logger.info(</span>"【注解:After】方法最后执行....."<span style="color: rgba(0, 0, 0, 1)">); } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * @Description: 环绕通知,环绕增强,相当于MethodInterceptor * @Title: around * </span><span style="color: rgba(128, 128, 128, 1)">@author</span><span style="color: rgba(0, 128, 0, 1)"> OnlyMate * @Date 2018年9月10日 下午3:52:56 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> pjp * </span><span style="color: rgba(128, 128, 128, 1)">@return</span> <span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> @Around(</span>"pointCut()"<span style="color: rgba(0, 0, 0, 1)">) </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> Object around(ProceedingJoinPoint pjp) { logger.info(</span>"【注解:Around . 环绕前】方法环绕start....."<span style="color: rgba(0, 0, 0, 1)">); </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">如果不执行这句,会不执行切面的Before方法及controller的业务方法</span> Object o =<span style="color: rgba(0, 0, 0, 1)"> pjp.proceed(); logger.info(</span>"【注解:Around. 环绕后】方法环绕proceed,结果是 :" +<span style="color: rgba(0, 0, 0, 1)"> o); </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> o; } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (Throwable e) { e.printStackTrace(); </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">; } }
}
B、使用 @AspectJ 风格的切面声明
/** * 自定义 AOP 配置类 * @ClassName: CustomAopConfigurer * @Description: TODO * @author OnlyMate * @Date 2018 年 9 月 7 日 下午 3:43:21 * */ @Configuration @ComponentScan("com.only.mate.springboot.aop") @EnableAspectJAutoProxy//开启 AspectJ 注解 public class CustomAopConfigurer { }
效果图
总结
AspectJ 在编译时就增强了目标对象,Spring AOP 的动态代理则是在每次运行时动态的增强,生成 AOP 代理对象,区别在于生成 AOP 代理对象的时机不同,相对来说 AspectJ 的静态代理方式具有更好的性能,但是 AspectJ 需要特定的编译器进行处理,而 Spring AOP 则无需特定的编译器处理。
详情:http://www.importnew.com/24305.html
想知道更多 @AspectJ 的使用,请前往 Spring Boot 实践——三种拦截器的创建 查看。