Spring中AOP实践中使用以及底层的CGLIB动态代理与JDK动态代理原理
一.AOP的介绍:
AOP是目前Spring框架中的核心知识点之一,在企业级中有着相当重要的作用。它是一种面向切面编程的重要思想。对于AOP我们需要了解一下几个关键的名词:
(1)切面(Aspect): 切面类中管理着增强的公共行为的代码(通知)和切入方式(切点)
(2)连接点(Join point):指定就是被增强的业务方法
(3)通知(Advice):就是需要增加到业务方法中的公共代码,通知有很多种类型分别可以在需要增加的业务方法不同位置进行执行(前置通知,后置通知,异常通知,返回通知,环绕通知)
(4)切点(PointCut):决定哪些方法需要被增强,哪些不需要被增强,结合切点表达式进行实现
二.AOP的使用:
2.1 AOP基础使用方式:
1.首先需要导入AspectJ的依赖(我们之后会说AspectJ AOP与 SpringAOP的区别):
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
方式一.使用Spring的API接口来做:
AfterLog.java:
package com.kuang.log;
import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;
public class AfterLog implements AfterReturningAdvice {
//returnValue:带有的返回值的
@Override
public void afterReturning(Object returnValue, Method method, Object[] objects, Object o1) throws Throwable {
System.out.println("执行了"+method.getName()+"方法,返回的结果为:"+returnValue);
}
}
beforeLog.java:
package com.kuang.log;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
public class beforeLog implements MethodBeforeAdvice {
//method:要执行目标对象的方法
//args:参数
//target:目标对象
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println(target.getClass().getName()+"的"+method.getName()+"被执行了");
}
}
UserService:
package com.kuang.service;
public interface UserService {
public void add();
public void delete();
public void update();
public void select();
}
UserServiceImpl:
package com.kuang.service;
public class UserServiceImpl implements UserService{
@Override
public void add() {
System.out.println("增加了一个用户");
}
@Override
public void delete() {
System.out.println("删除了一个用户");
}
@Override
public void update() {
System.out.println("修改了一个用户");
}
@Override
public void select() {
System.out.println("查询了一个用户");
}
}
xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册bean-->
<bean id="userService" class="com.kuang.service.UserServiceImpl"/>
<bean id="beforelog" class="com.kuang.log.beforeLog"/>
<bean id="afterlog" class="com.kuang.log.AfterLog"/>
<!-- 方式一:使用原生的Spring API接口-->
<!--配置AOP:需要导入aop的约束-->
<aop:config>
<!--首先需要一个切入点,expression:表达式,execution(要执行的位置)-->
<aop:pointcut id="pointcut" expression="execution(* com.kuang.service.UserServiceImpl.*(..))"/>
<aop:advisor advice-ref="beforelog" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="afterlog" pointcut-ref="pointcut"/>
</aop:config>
</beans>
方式二.使用自定义类来实现AOP:
1.自定义类(切面):
package com.kuang.diy;
public class DiyPointCut {
public void before(){
System.out.println("方法执行前");
}
public void after(){
System.out.println("方法执行后");
}
}
2.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册bean-->
<bean id="userService" class="com.kuang.service.UserServiceImpl"/>
<!--方式二:自定义的类-->
<bean id="diy" class="com.kuang.diy.DiyPointCut"/>
<aop:config>
<!--增加一个切面 ref 要引用的类-->
<aop:aspect ref="diy">
<!--切入点-->
<aop:pointcut id="point" expression="execution(* com.kuang.service.UserServiceImpl.*(..))"/>
<!--通知-->
<aop:before method="before" pointcut-ref="point"/>
<aop:after method="after" pointcut-ref="point"/>
</aop:aspect>
</aop:config>
</beans>
方式三.使用注解的方式实现AOP:
1.AnnotationPointCut:
package com.kuang.diy;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
//方式三:使用注解方式来实现AOP
@Aspect//标志为一个切面
public class AnnotationPointCut {
@Before("execution(* com.kuang.service.UserServiceImpl.*(..)))")
public void defore(){
System.out.println("方法执行前");
}
@After("execution(* com.kuang.service.UserServiceImpl.*(..)))")
public void after(){
System.out.println("方法执行后");
}
}
2.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
">
<!--注册bean-->
<bean id="userService" class="com.kuang.service.UserServiceImpl"/>
<!--方式三:使用注解来实现AOP-->
<bean id="annotationPointCut" class="com.kuang.diy.AnnotationPointCut"/>
<!--开启注解的支持-->
<aop:aspectj-autoproxy/>
</beans>
常用的一些注解有:
@Aspect @Before @After @Around .....
2.2企业级开发中的使用方式:
Spring AOP可以帮助我们Java在不修改源代码的前提下实现功能的增强,其底层实现基于Java的动态代理或者是CGLIB。但是这样使用往往不灵活,因为并不是service中所有的方法都需要被增强。其实受到Spring声明式事务@Transactional的启发,我们在项目中利用自定义注解可以实现大量的共性需求。
应用场景:
需要灵活使用共性需求的地方都可以使用该方案:
- 收集上报指定关键方法的入参,执行,返回结果等关键性信息,用作后期的调优
- 关键方法在幂等性前置校验(基于本地消息表)
- 类似于Spring-Retry模块,提供关键方法多次调用重试机制
- 提供关键方法自定义的快速熔断,服务降级等职责
- 关键方法的共性入参校验
- 关键方法在执行后的扩展行为,例如记录日志,启动其他的任务等。
开发过程:
1.引入AspectJ依赖:
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
2.新建自定义注解,利用@interface关键字自定义注解:
//该注解用在方法上:
@Target(ElementType.METHOD)
//Retention作用是定义被它所注解保留时间,RUNTIME为运行时:
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodExporter{
}
3.开发切面类:
@Aspect
@Component
@Slf4j
public class MethodExporterAspect{
//@Around环绕通知,最强大的通知类型,可以控制方法入参,执行,返回结果等各方面的具体细节:
//任何增加了@MethodExporter的目标方法都将在执行方法前先执行该切面方法:
@Around("@annotation(com.liuyuchneg.MethodExporter)")
public Object methodExporter(ProceedingJoinPoint joinPoint) throws Throwable
{
long st = new Date().getTime();
//执行目标方法,获取方法返回值:
Object proceed = joinPoint.proceed();
long et = new Date().getTime();
ObjectMapper mapper = new ObjectMapper();
//将入参序列化:
String jsonParam = mapper.writeValueAsString(joinPoint.getArgs());
//将返回结果序列化:
String jsonResult = null;
if(proceed != null){
jsonResult = mapper.writeValueAsString(proceed);
}else{
jsonResult = null;
}
//模拟上报过程:
log.debug(.....);
return proceed;
}
}
4.使用,在目标方法上增加自定义注解:
@RestController
public class SampleController{
@MethodExporter
@GetMapping("/list")
public Map list(int page, int rows){
//方法中真正执行的逻辑:
.......
}
}
运行访问该方法在控制台可以看到:我们可以将这些信息统一上传的远程的服务当中用以记录方法调用的运行时常等一些信息,为了后期的优化。
三.JDK动态代理与CGLIB动态代理:
AOP的源码中用到了两种动态代理来实现拦截切入的功能:jdk动态代理于cglib动态代理。两种方法同时存在各有优势。jdk动态代理是由java内部的反射机制来实现的,cglib动态代理则是通过借助asm来实现的。我们先来看看两种动态实现的步骤,再来做比较:
3.1 jdk动态代理:
1.提供统一的接口,这个是jdk动态代理的前提:
public interface Service{
public void add();
public void update();
}
2.统一接口的实现类,我们就是在add与update方法前后进行拦截实现切面逻辑:
public serviceImpl implements Service{
public void add(){
System.out.println("执行add的具体逻辑");
}
public void update(){
System.out.println("执行update的具体逻辑");
}
}
3.实现动态代理接口InvocationHandler,在invoke方法加入切面的逻辑:
public class MyInvocationHandler implements InvocationHandler {
private Object target;
MyInvocationHandler() {
super();
}
MyInvocationHandler(Object target) {
super();
this.target = target;
}
//生成得到代理类
public Object getProxy(){
//这里的this代指ProxyInvocationHandler,因为其实现了InvocationHandler
return Proxy.newProxyInstance(this.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this);
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// 程序执行前加入逻辑,MethodBeforeAdviceInterceptor
System.out.println("beforeMethod-----------------------------");
// 程序执行
Object result = method.invoke(target, args);
// 程序执行后加入逻辑,MethodAfterAdviceInterceptor
System.out.println("afterMethod------------------------------");
return result;
}
}
4.进行测试:
public class Test {
public static void main(String[] args) {
Service service = new serviceImpl();
MyInvocationHandler handler = new MyInvocationHandler(service);
// Proxy为InvocationHandler实现类动态创建一个符合某一接口的代理实例
Service serviceProxy = handler.getProxy();
// 由动态生成的代理对象来aServiceProxy 代理执行程序,其中aServiceProxy 符合Service接口
serviceProxy.add();
serviceProxy.update();
}
}
所得到的结果为:
beforeMethod-----------------------------
System.out.println("执行add的具体逻辑");
afterMethod------------------------------
beforeMethod-----------------------------
System.out.println("执行update的具体逻辑");
afterMethod------------------------------
可以看到在目标类serviceImpl的add与update方法前后已经加入了自定义的切面逻辑,AOP的拦截机制生效了。
3.2 cglib动态代理:
1.对于cglib来并不需要实现目标类统一的接口:
public class Base {
/**
* 一个模拟的add方法
*/
public void add() {
System.out.println("add ------------");
}
}
2.实现动态代理CglibProxy需要实现MethodInterceptor接口并实现intercept方法
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class cglibProxy implements MethodInterceptor {
@Override
public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("before-------------");
proxy.invokeSuper(object,args);
System.out.println("after-------------");
return null;
}
}
3.获取增强的目标类的工厂Factory,其中增强的方法类对象是由Enhancer来实现的:
import org.springframework.cglib.proxy.Enhancer;
public class Factory {
/**
* 获得增强之后的目标类,即添加了切入逻辑advice之后的目标类
*
* @param proxy
* @return
*/
public static Base getInstance(cglibProxy proxy) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Base.class);
//回调方法的参数为代理类对象CglibProxy,最后增强目标类调用的是代理类对象CglibProxy中的intercept方法
enhancer.setCallback(proxy);
// 此刻,base不是单纯的目标类,而是增强过的目标类
Base base = (Base) enhancer.create();
return base;
}
}
4.测试:
public class Test {
public static void main(String[] args) {
cglibProxy proxy = new cglibProxy();
// base为生成的增强过的目标类
Base base = Factory.getInstance(proxy);
base.add();
}
}
输出如下:
before-------------
add ------------
after-------------
3.3 比较JDK动态代理与cglib动态代理的区别:(面试重点)
(1)是否使用接口:
1)jdk动态代理只提供接口的代理,不支持类的代理
2)如果代理类没有实现接口,那么SpringAOP会选择使用CGLIB来动态代理目标类
(2)生成方式:
1)jdk会在运行时为目标类生成一个动态代理类$proxy*.class
2)cglib底层是通过ASM在运行时动态的生成目标类的一个子类
(3)实现方式:
1)jdk是代理类实现类目标类接口,并且代理类会实现接口所有的方法并增强
2)cglib会重写父类所有的方法增强代码
(4)调用方式:
1)jdk会通过代理类先去调用处理类进行增强,再通过反射方式进行调用目标方法从而实现AOP
2)cglib调用时先通过代理类进行增强,再直接调用父类对应的方法进行调用目标方法,从而实现AOP.
四.SpringAOP与AspectJAOP的区别与联系:
也是面试重点部分:
1.联系:
(1)当在Spring中要使用@AspectJ,@Before等这些注解时候,就需要添加AspectJ的相关的依赖:
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
(2)SpringAOP提供了AspectJ的支持,但只用到了AspectJ的切点解析和匹配。
2.区别:
AOP实现的关键在于代理模式,AOP代理模式主要分成静态代理与动态代理。静态代理代表是AspectJ,动态代理则以SpringAOP为代表
(1)SpringAOP使用的动态代理,它是基于动态代理来实现的,默认地,如果使用接口,就会使用JDK提供的动态代理来实现,如果没有接口就会使用CGlib来实现
(2)AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP的代理类,因此也称之为编译时增强,它会在编译阶段将AspectJ织入到Java字节码中,运行时候就是增强之后的AOP对象,以下是织入的时机:
1)Compile-time-weaving:编译期织入
2)Post-compile-weaving:编译后织入,也就是已经生成.class文件后进行织入动态改变.class文件代码
3)load-time-weaving:指的是在加载类的时候进行织入,我们可以自定义类加载器来在被织入类加载到JVM前去1111对它进行加载。
(3)性能相比,SpringAOP是基于代理实现的,在容器启动时候需要生成代理实例,在方法调用上也会增加栈的深度,使得SpringAOP性能不如AspectJ这么好,但是SpringAop
是致力于解决企业级开发中最普遍的AOP需求,所以并不需要像AspectJ做的那么完善。