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"); } /** * 解密 * @Title: decode * @Description: TODO * @Date 2018年9月7日 下午2:30:11 * @author OnlyMate */ public void decode() { logger.info("CustomAuthorityAspect ==> decode method: decode data"); } }
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); /** * @Description: 定义切入点 * @Title: pointCut * @author OnlyMate * @Date 2018年9月10日 下午3:52:17 */ //被注解CustomAopAnnotation表示的方法 //@Pointcut("@annotation(com.only.mate.springboot.annotation.CustomAopAnnotation") @Pointcut("execution(public * com.only.mate.springboot.controller.*.*(..))") public void pointCut(){ } /** * @Description: 定义前置通知 * @Title: before * @author OnlyMate * @Date 2018年9月10日 下午3:52:23 * @param joinPoint * @throws Throwable */ @Before("pointCut()") public void before(JoinPoint joinPoint) throws Throwable { // 接收到请求,记录请求内容 logger.info("【注解:Before】------------------切面 before"); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 记录下请求内容 logger.info("【注解:Before】浏览器输入的网址=URL : " + request.getRequestURL().toString()); logger.info("【注解:Before】HTTP_METHOD : " + request.getMethod()); logger.info("【注解:Before】IP : " + request.getRemoteAddr()); logger.info("【注解:Before】执行的业务方法名=CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName()); logger.info("【注解:Before】业务方法获得的参数=ARGS : " + Arrays.toString(joinPoint.getArgs())); } /** * @Description: 后置返回通知 * @Title: afterReturning * @author OnlyMate * @Date 2018年9月10日 下午3:52:30 * @param ret * @throws Throwable */ @AfterReturning(returning = "ret", pointcut = "pointCut()") public void afterReturning(Object ret) throws Throwable { // 处理完请求,返回内容 logger.info("【注解:AfterReturning】这个会在切面最后的最后打印,方法的返回值 : " + ret); } /** * @Description: 后置异常通知 * @Title: afterThrowing * @author OnlyMate * @Date 2018年9月10日 下午3:52:37 * @param jp */ @AfterThrowing("pointCut()") public void afterThrowing(JoinPoint jp){ logger.info("【注解:AfterThrowing】方法异常时执行....."); } /** * @Description: 后置最终通知,final增强,不管是抛出异常或者正常退出都会执行 * @Title: after * @author OnlyMate * @Date 2018年9月10日 下午3:52:48 * @param jp */ @After("pointCut()") public void after(JoinPoint jp){ logger.info("【注解:After】方法最后执行....."); } /** * @Description: 环绕通知,环绕增强,相当于MethodInterceptor * @Title: around * @author OnlyMate * @Date 2018年9月10日 下午3:52:56 * @param pjp * @return */ @Around("pointCut()") public Object around(ProceedingJoinPoint pjp) { logger.info("【注解:Around . 环绕前】方法环绕start....."); try { //如果不执行这句,会不执行切面的Before方法及controller的业务方法 Object o = pjp.proceed(); logger.info("【注解:Around. 环绕后】方法环绕proceed,结果是 :" + o); return o; } catch (Throwable e) { e.printStackTrace(); return null; } } }
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则无需特定的编译器处理。