SpringBoot AOP浅析

SpingAop

面向切面编程:相对于OOP面向对象编程,Spring的AOP的存在目的是为了解耦。AOP可以让一组类共享相同的行为。在OOP中只能继承和实现接口,且类继承只能单继承,阻碍更多行为添加到一组类上,AOP弥补了OOP的不足,让业务逻辑关注业务本身,不用去关心其它的事情,比如事务。有如下优点:

  1. 集中的处理某一关注点/横切逻辑
  2. 可以很方便的添加/删除 关注点
  3. 侵入性少,增强代码的可读性和可维护性

业务场景

  1. 事务控制:给目标方法添加前后的声明周期,事务开启、回滚、提交等
  2. 日志记录:给目标方法添加日志记录,记录方法的执行时间、接收参数、返回结果等
  3. 权限控制:给目标方法添加权限控制,在方法执行前,判断是否有执行该方法的权限
  4. 异常处理:对于目标方法的返回异常进行处理,给出友善的错误提示
  5. 国际化:对于返回结果进行处理,如中英文替换等

常用术语

  • 通知(Advice)

    有的地方又叫做增强,需要完成的工作叫做通知,入事务控制、日志记录等,你要先定义好,等到使用的时候再去用

  • 切点(Poingcut)

    一个类中的所有方法都可以是切点,根据需要筛选出需要添加通知的方法,切点是为了找到需要增强的方法

    最常用的切点:execution(* com.jiuxian..service.*.*(..))

    • execution 表达式的主体
    • 第一个* 代表任意的返回值
    • com.jiuxian aop所横切的包名
    • 包后面… 表示当前包及其子包
    • 第二个* 表示类名,代表所有类
    • .*(…) 表示任何方法,括号代表参数 … 表示任意参数
  • 切面(Aspect)

    其实就是切点和通知的结合,通知和切点定义了切面的全部内容,它是干什么的,什么时候在哪执行

  • 织入(Weaving)

    把切面加入程序代码的过程。切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多个点可以进行织入:

    • 编译期: 切面在目标类编译时被织入,这种方式需要特殊的编译器
    • 类加载期: 切面在目标类加载到JVM时被织入,这种方式需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码
    • 运行期: 切面在应用运行的某个时刻被织入,一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象,Spring AOP就是以这种方式织入切面的。

AopDemo

  1. 添加spring-aop依赖
<!-- aop依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 定义一个切面
package com.nubo.jfs.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
1. @description 日志拦截
2. @author liubin
3. @date 20/4/21 11:45
*/
@Aspect
@Component
public class LogAspect {
   
   @Pointcut("execution(public * com.nubo.jfs.service.*.*(..))")
   public void pointcut() {
   }

   @Around("pointcut()")
   public void around(ProceedingJoinPoint point) {
       long t1 = System.currentTimeMillis();
       System.out.println("日志记录开始");
       try {
           point.proceed();
       } catch (Throwable throwable) {
           throwable.printStackTrace();
       }
       long t2 = System.currentTimeMillis();
       System.out.println("日志记录结束,耗时:" + (t2 - t1) + "ms");
   }
}
  1. 五种通知(增强)代码实现
	@Before("pointcut()")
    public void beginTransaction() {
        System.out.println("before beginTransaction");
    }

    @After("pointcut()")
    public void commit() {
        System.out.println("after commit");
    }

    @AfterReturning(pointcut = "pointcut()", returning = "returnObject")
    public void afterReturning(JoinPoint joinPoint, Object returnObject) {
        System.out.println("afterReturning");
    }

    @AfterThrowing("pointcut()")
    public void afterThrowing() {
        System.out.println("afterThrowing afterThrowing  rollback");
    }
    
	@Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            System.out.println("around");
            return joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
            throw e;
        } finally {
            System.out.println("around");
        }
    }
  1. 注解的方式注入
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auth {
    boolean hasAuth() default true;
}
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 - @description 权限拦截
 - @author liubin
 - @date 20/4/21 15:28
 */
@Aspect
@Component
@Order(1)
public class AuthAspect {

    @Pointcut(value = "@annotation(auth)", argNames = "auth")
    public void pointcut(Auth auth) {
    }

    @Around(value = "pointcut(auth)", argNames = "joinPoint,auth")
    public void aroung(ProceedingJoinPoint joinPoint, Auth auth) {
        if (auth.hasAuth()) {
            System.out.println("have auth...");
            try {
                joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        } else {
            System.out.println("not have auth...");
        }
    }
}

AOP底层原理

在spring对实例进行初始化的时候,最后会调用

  • AbstractAutowireCapableBeanFactory.createBean() 创建bean
  • AbstractAutowireCapableBeanFactory.initializeBean() 初始化bean对象
  • AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization() 注入后续属性
  • AbstractAutoProxyCreator.postProcessAfterInitialization() 织入Aop通知
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
		implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
	protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
			@Nullable Object[] specificInterceptors, TargetSource targetSource) {
		if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
			AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) 				   this.beanFactory, beanName, beanClass);
		}

		ProxyFactory proxyFactory = new ProxyFactory();
		proxyFactory.copyFrom(this);
		// 如果配置了属性spring.aop.proxy-target-class = true,则强制使用cglib代理
		if (!proxyFactory.isProxyTargetClass()) {
			// 是否需要代理类或者代理接口
			if (shouldProxyTargetClass(beanClass, beanName)) {
				proxyFactory.setProxyTargetClass(true);
			}
			else {// 如果代理的是接口,则判断是否实现了接口,没有实现接口使用代理类
				evaluateProxyInterfaces(beanClass, proxyFactory);
			}
		}

		Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
		proxyFactory.addAdvisors(advisors);
		proxyFactory.setTargetSource(targetSource);
		customizeProxyFactory(proxyFactory);

		proxyFactory.setFrozen(this.freezeProxy);
		if (advisorsPreFiltered()) {
			proxyFactory.setPreFiltered(true);
		}

		return proxyFactory.getProxy(getProxyClassLoader());
	}
}

根据代理类型获取代理方式,代理类使用cgilb动态代理,代理接口使用jdk动态代理

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

	@Override
	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		// isOptimize默认false || 是代理类? || 没继承接口?
		if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
			Class<?> targetClass = config.getTargetClass();
			if (targetClass == null) {
				throw new AopConfigException("TargetSource cannot determine target class: " +
						"Either an interface or a target is required for proxy creation.");
			}
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
				return new JdkDynamicAopProxy(config);
			}
			return new ObjenesisCglibAopProxy(config);
		}
		else {
			return new JdkDynamicAopProxy(config);
		}
	}

	/**
	 * Determine whether the supplied {@link AdvisedSupport} has only the
	 * {@link org.springframework.aop.SpringProxy} interface specified
	 * (or no proxy interfaces specified at all).
	 */
	private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
		Class<?>[] ifcs = config.getProxiedInterfaces();
		return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0])));
	}

}

从上源码可以看到,SpringAop的底层实现是通过jdk动态代理和Cglib代理来实现的,也可以通过参数控制使用哪种代理模式

spring.aop.proxy-target-class = true/false

spring默认使用jdk动态代理,而springboot默认使用的cglib动态代理

spingboot官方是这样解释的:他们认为使用cglib更不容易出现转换错误

This was changed in 1.4 (see 5423). We’ve generally found cglib proxies less likely to cause unexpected cast exceptions.

SpringBoot 默认的配置文件中是这样显示的 spring-configuration-metadata.json

{
      "name": "spring.aop.auto",
      "type": "java.lang.Boolean",
      "description": "Add @EnableAspectJAutoProxy.",
      "defaultValue": true
    },
    {
      "name": "spring.aop.proxy-target-class",
      "type": "java.lang.Boolean",
      "description": "Whether subclass-based (CGLIB) proxies are to be created (true), as opposed to standard Java interface-based proxies (false).",
      "defaultValue": true
    }

我们发现 SpringBoot 中的注解@EnableAspectJAutoProxy 或者@EnableTransactionManagement 默认的proxyTargetClass 就是false,说明这上面的设置并未生效,

@EnableAspectJAutoProxy(proxyTargetClass = false)
@EnableTransactionManagement(proxyTargetClass = false)

如果想要走jdk动态代理,还需要在配置文件设置设置

spring.aop.proxy-target-class = false

以下是走哪个代理方式的配置方式

配置spring.aop.auto配置 spring.aop.proxy-target-class注解的proxyTargetClass代理 方式
truetruetrueCglib
truefalsetrueCglib
truetruefalseCglib
truefalsefalseJDK动态代理
falsetruetrueCglib
falsefalsetrueCglib
falsetruefalseJDK动态代理
falsefalsefalseJDK动态代理

代理模式

静态代理

由程序员创建类再对其进行编译,在程序运行前代理类的的.class就已经存在了

  1. 继承代理

公共的取放的接口类

/**
 * @description 取放的接口
 * @author liubin
 * @date 20/4/21 09:12
 */
public interface TakeService {

    void takeIn();

    void takeOut();
}

取药拿药的操作类

/**
 * @description 拿药
 * @author liubin
 * @date 20/4/21 09:22
 */
@Service
public class DrugServiceImpl implements TakeService {
    @Override
    public void takeIn() {
        // 补药
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("take in drug...");
    }

    @Override
    public void takeOut() {
        // 拿药
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("take out drug...");
    }
}

对取药拿药的操作织入日志记录的功能

/**
 * @description 日志记录
 * @author liubin
 * @date 20/4/21 09:33
 */
public class ExtendDrugLogProxy extends DrugServiceImpl {

    @Override
    public void takeIn() {
        long t1 = System.currentTimeMillis();
        System.out.println("日志记录开始");
        super.takeIn();
        long t2 = System.currentTimeMillis();
        System.out.println("日志记录结束,耗时:" + (t2 - t1) + "ms");
    }
}

  1. 聚合代理

使用聚合代理的方式对拿药取药的操作织入日志记录

/**
 - @description 日志记录
 - @author liubin
 - @date 20/4/21 09:50
 */
public class ImplementDrugLogProxy implements TakeService {

    private TakeService takeService;

    public ImplementDrugLogProxy(TakeService takeService) {
        this.takeService = takeService;
    }

    @Override
    public void takeIn() {
        long t1 = System.currentTimeMillis();
        System.out.println("日志记录开始");
        takeService.takeIn();
        long t2 = System.currentTimeMillis();
        System.out.println("日志记录结束,耗时:" + (t2 - t1) + "ms");
    }

    @Override
    public void takeOut() {
        long t1 = System.currentTimeMillis();
        System.out.println("日志记录开始");
        takeService.takeOut();
        long t2 = System.currentTimeMillis();
        System.out.println("日志记录结束,耗时:" + (t2 - t1) + "ms");
    }
}
  • 总结

  • 继承方式:每多一个切点,多写一个通知;通知顺序改变,代理类需要重构

  • 聚合方式:每多一个切点,多写一个通知;通知顺序改变,只需在调用时改变顺序

动态代理

与静态代理相反,动态代理是在程序编译或者运行的的时候才生成代理类,更加灵活和轻便

1. jdk动态代理

jdk动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用业务方法前调用InvocationHandler处理。代理类必须实现InvocationHandler接口,并且,JDK动态代理只能代理实现了接口的类,没有实现接口的类是不能实现JDK动态代理。结合下面代码来看就比较清晰了。

/**
 * @description jdk动态代理
 * @author liubin
 * @date 20/4/21 10:19
 */
public class JdkLogProxy implements InvocationHandler {
    //被代理的对象
    private Object target;
    public JdkLogProxy(Object target) {
        this.target = target;
    }
    /**
     * @param proxy  代理对象
     * @param method  增强的方法
     * @param args 方法参数
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long t1 = System.currentTimeMillis();
        System.out.println("日志记录开始");
        Object result = method.invoke(target, args);
        long t2 = System.currentTimeMillis();
        System.out.println("日志记录结束,耗时:" + (t2 - t1) + "ms");
        return result;
    }
}

2. Cglib动态代理

cglib是针对类来实现代理的,它会对目标类产生一个代理子类,通过方法拦截技术对过滤父类的方法调用。代理子类需要实现MethodInterceptor接口。

/**
 * @description cglib动态代理
 * @author liubin
 * @date 20/4/21 11:11
 */
public class CglibLogProxy implements MethodInterceptor {
    /**
     * @param object 需要增强的对象
     * @param method 表示拦截的方法
     * @param objects 数组表示参数列表,基本数据类型需要传入其包装类型,如int-->Integer、long-Long、double-->Double
     * @param methodProxy 表示对方法的代理,invokeSuper方法表示对被代理对象方法的调用
     * @return java.lang.Object
     */
    @Override
    public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        long t1 = System.currentTimeMillis();
        System.out.println("日志记录开始");
        Object result = methodProxy.invokeSuper(object, objects);
        long t2 = System.currentTimeMillis();
        System.out.println("日志记录结束,耗时:" + (t2 - t1) + "ms");
        return result;
    }
}
区别jdk动态代理Cglib动态代理
实现原理使用聚合的方式使用继承的方式
不能代理的情况没有实现接口类final类型的类或方法

在jdk6、jdk7、jdk8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,只有当进行大量调用的时候,jdk6和jdk7比CGLIB代理效率低一点,但是到jdk8的时候,jdk代理效率高于CGLIB代理,总之,每一次jdk版本升级,jdk代理效率都得到提升,而CGLIB代理消息确有点跟不上步伐。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值