Spring世界--AOP解析:AspectJ

206 阅读9分钟

AOP是什么

AOP(Aspect Orient Programming),面向切面编程,把原先的静态思考转换成动态的程序运行过程,可以在于运行期间的动态代理实现,属于Spring世界的重要一员,降低了代码的耦合度,提高了程序的可冲用性,提高了开发效率,属于面向对象变成的一种补充。

以一个常见的事务与业务混合编写的代码为例子,问题就在于耦合度过高,除了关心业务代码外还要关心事务代码的编写(或其他切面)

    public void action(){
        //业务与事务耦合度太高
        try {
            System.out.println("开始事务");
            System.out.println("进行诸多业务执行");
            System.out.println("事务提交");
        } catch (Exception e) {
            System.out.println("事务回滚");
        }
    }

稍微改进一下,通过子类代理的方式(装饰者模式)

//要被代理的类
class Action {
    public void action(){
        System.out.println("进行诸多业务执行");
    }
}

//代理类
class Proxy extends Action{
    @Override
    public void action(){
        try{
            System.out.println("开始事务");
            super.action(); //开始业务
            System.out.println("事务提交");
        }catch(Exception e){
            System.out.println("事务回滚");
        }
    }
}

目前这种方式的进行了业务和事务的分离,父类中只有纯净的业务代码

再改进下,使用静态代理拆分业务和切面,业务和切面都实现相同的接口,接口是核心

//定义接口
class interface Action{
    void action();
}

//业务接口的实现
interface ActionImpl{
    @Override
    public void action(){
        System.out.println("进行诸多业务执行");
    }
}

//最终的代理类
class Proxy implements Action{
    private Action target;
    
    //定义接口类型的成员变量,通过构造方法传入目标代理对象
    public Proxy(Action target){
        this.traget = target;
    }

    @Override
    public void action(){
        try{
            System.out.println("事务开始");
            target.action(); //调用被代理的目标方法
            System.out.println("事务提交");
        }catch(Exception e){
            System.out.println("事务回滚");
        }
    }
}

这个实现的好处在于,只要传入的目标对象实现了Action接口,那么就可以被代理,灵活切换目标代理对象,切面仍然可以继续和业务结合工作,缺点是切面是"死的",切面可能还有很多比如日志,权限等等

再改进一下,让切面也可以灵活切换

定义切面的接口,分别抽象出业务执行前,业务执行后,业务异常时三种切入时机

// java8开始提供了接口默认实现的特性,想实现哪个时机的切面方法更灵活,不用全实现
interface Aop{
    default before(){}
    
    default after(){}
    
    default exception(){}
}

class LogAop implements Aop{
    @Override
    public void before(){
        System.out.println("业务执行前日志");
    }
    
    @Override
    public void after(){
        System.out.println("业务执行后日志");
    }
    
    @Override
    public void exception(){
        System.out.println("发生异常时日志");
    }
}

class Proxy implements Action{
    private Action target;
    private Aop aop;
    
    //定义接口类型的成员变量,通过构造方法传入目标代理对象
    //再定义一个成员变量类型为Aop类型,通过构造方法传入切面
    public Proxy(Action target,Aop aop){
        this.traget = target;
        this.aop = aop;
    }

    @Override
    public void action(){
        try{
            aop.before(); //切面方法调用
            target.action(); //调用被代理的目标方法
            aop.after(); //切面方法调用
        }catch(Exception e){
            aop.exception(); //切面方法调用
        }
    }
}

现在代理对象可以灵活的传入切面对象和业务对象,代理就能正常的按照预期进行工作,由于代理对象也实现了业务接口,所以可以进行俄罗斯套娃进行多个切面组织

但仍然不够灵活,目前的业务是写死的action,预期应该要可以代理目前对象的任意方法,进行动态代理的改造

定义一个代理工厂返回代理对象

class ProxyFactor{
    public static Object getInstance(Action target,Aop aop){
        //JDK提供的代理需要提供三个参数,类加载器,目标对象实现的所有接口,代理功能具体实现类
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader, //类加载器
            target.getClass().getInterfaces(), //所有实现的接口集合
            (agent, method, args) -> { //三个参数分别为被生成的代理对象
                //切记这里不可调用agent的任何方法,否则将死循环
                // 这个函数的返回就是代理对象调用方法的返回值
                Object result = null;
                try{
                    aop.before();
                    result = method.invoke(target,args);
                    aop.after();
                }catch(Exception e){
                    aop.exception();
                }
                return result;
            } //这里使用lambda表达式实现匿名内部类InvocationHandler
        );
    }
}

利用JDK提供的动态代理,便完成了接口与切面的灵活整合,这种方式的优点是方便,不需要依赖任何第三方库,缺点是功能受限,只能用于接口,需要处理类,则需要使用CGLIB库进行字节码增强

一些AOP的常用术语

  • 切面:重复,公共,通用的功能称为切面,例如日志,事务,权限
  • 连接点:目标方法,因为在目标方法中要实现目标方法的功能和功能
  • 切入点(Pointcut):指定切入点,多个连接点构成切入点,切入点可以是一个目标方法,也可以是类中的所有的方法,可以是某个包下的所有类中的方法
  • 目标对象:操作谁,谁就是目标对象
  • 通知(Adive):指定切入的时机,是在目标方法执行前还是执行后还是出现异常后,还是环绕目标方法切入切面功能

Spring原生就提供了Aop的实现

常用的有

  • Beofre通知:在目标方法调用前调用(org.springframework.aop.MethodBeforeAdvice
  • After通知:在目标方法被调用后调用(org.springframework.aop.AfterReturnAdvice
  • Throws通知:目标方法抛出异常时调用(org.springframework.aop.ThrowsAdvice
  • Around通知:拦截对目标对象方法的调用(org.aopalliance.intercept.MethodInterceptor

以xml配置的形式使用Spring提供的IOC

模拟一个服务接口

public interface Service{
    void doSomething();
}

// 对接口进行实现
public class UserServiceImpl implements Service{
    @Override
    public void doSomething(){
        System.out.println("do something!");
    }
}

再定义一个bean,去实现MethodBeforeAdviceAfterReturningAdvice接口

public class LogAdvice implements MethodBeforeAdvice, AfterReturningAdvice {

    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd");
        System.out.println("系统日志:" + sf.format(new Date()) + " :方法名为->" + method.getName() +
                " 方法参数为->" + Arrays.toString(objects));
    }

    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) {
        System.out.println("系统日志:方法调用结束");
    }
}

Springxml中配置bean,主要在于org.springframework.aop.framework.ProxyFactoryBean按属性注入

<bean id="logAdvice" class="cn.mgl.aop.LogAdvice"></bean>
<bean id="serviceTarget" class="cn.mgl.aop.UserServiceImpl"></bean>
<bean id="userService" class="org.springframework.aop.framework.ProxyFactoryBean">
  <property name="interfaces" value="cn.mgl.aop.Service"></property>
  <property name="interceptorNames">
    <list>
      <value>logAdvice</value>
    </list>
  </property>
  <property name="target" ref="serviceTarget"></property>
</bean>

使用时

ClassPathXmlApplicationContext ct = new ClassPathXmlApplicationContext("classpath:application.xml");
Service service = (Service)ct.getBean("userService");
service.doSomething();

执行后
image.png

AspectJ

优秀的面向切面的框架,扩展了Java语言,提供了强大的切面功能,在Spring中使用AOP时一般使用AspectJ的实现方式,支持注解式开发。
AspectJ中常用的通知

  • 前置通知,调用前执行 @Before
  • 后置通知,调用后执行 @After
  • 环绕通知,一般用于事务 @Around
  • 最终通知,不管成功失败都执行 @After
  • 定义切入点@Pointcut
Aspect切入点公式

规范的公式为execution(访问权限 方法返回值 方法声明(参数) 异常类型)
简化后的公式为execution(方法返回值 方法声明(参数))

特殊符号

  • *表示通配符,任意个字符
  • ..两个点
    • 如果出现在方法的参数中,表示任意参数
    • 如果出现在路径中,则表示本路径及其所有的子路径
实例作用
execution(public * *(..))任意的公共方法
execution(* set*(..)任意的setter的方法
execution(* com.abc.service.*.*(..))任意返回值,com.abc.service包下中所有类,任意方法名,任意参数
execution(* *..service.*.*(..))任意目录下的service包中的所有类,任意方法名,任意参数
execution(* *.service.*.*(..))任意子包(注意层级)为service的所有类,任意方法名,任意参数

Spring将AspectJ整合到了自己的框架中,在pom.xml中引入AspectJ

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
</dependency>

定义一个配置Bean
这里的EnableAspectJAutoProxy注解很重要,只有添加后,Spring才会去扫描所有带@Aspect注解的切面对象,同时需要满足对象是Bean,所有一般@Aspect@Component配合使用,否则无效,通过扫描@Aspect定义的切面类,再从切入点找到目标类的目标方法,再由通知类型找到切入时间点。其中有一个proxyTargetClass属性,默认是falsefalse则使用JDK的动态代理,true则使用CGLIB。顺提一下,在SpringBoot2.0后,自动装配的配置中,默认为true,可以通过配置文件的spring.aop.proxy-target-class修改

@Configuration
@ComponentScan("cn.mgl.aop")
@EnableAspectJAutoProxy
public class MainConfig {
}

这里的切面表达式指定了一个全限定类名的任意方法任意参数,通知类型为@Before

所有的通知方法(通知方法的方法名不重要)都有一个参数类型为JoinPoint,意味切面表达式本身,可以通过对象获取目标调用方法的方法名,方法参数,目标对象

@Aspect
@Component
public class LogAspect {
    @Before("execution(* cn.mgl.aop.service.UserServiceImpl.*(..))")
    public void beforeAOP(JoinPoint jp) {
        System.out.println("前置通知:");
        System.out.println("调用方法的签名:" + jp.getSignature());
        System.out.println("调用方法的参数:" + Arrays.toString(jp.getArgs()));
    }
}

检验切面织入是否成功

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(MainConfig.class);
Service userService = (Service) ctx.getBean("userService");
userService.doSomething();

调用后
image.png

@AfterReturning 后置通知

在目标方法执行之后执行,由于是执行之后执行,所以可以获取目标方法的返回值,该主要有一个returning属性,用于指定接受方法返回值的变量名,该注解除了JoinPoint参数外,还可以有一个接受返回值的参数,最好是Object类型,因为返回值的类型可能是任意类型

@AfterReturning(value = "execution(* cn.mgl.aop.service.UserServiceImpl.*(..))", returning = "result")
public void afterAOP(JoinPoint jp, Object result) {
    //result要与注解中的定义一致
    System.out.println("方法的返回值:" + result);
}

@Around 环绕通知

会在目标方法执行之前以及执行之后执行,被注解为环绕怎强的方法要有返回值,Object类型,并且方法可以包含一个ProceedingJoinPoint类型的参数,对象带有一个proceed方法,作用是执行目标方法,若目标方法有返回值,则该函数的返回值就是目标方法的返回值,最后环绕方法再将返回值return,实质上就是对方法的一次拦截处理。

把模拟的Service接口和实现的doSomething方法返回值改为Object并返回null,接着使用@Around增强方法

@Around("execution(* *..UserServiceImpl.*(..))")
public Object aroundAOP(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("环绕增强开始:");
    Object result= pjp.proceed(); //一定要调用,不然相当于目标方法没有执行过,等价于动态代理中的method.invoke()
    System.out.println("环绕增强结束");
    return "123456";
}

最后看看输出结果,本来返回null的结果被篡改成功 image.png

@After 最终通知

无论目标方法是否抛出异常都会被执行,类似于try catch finallyfinally

@Around("execution(* *..UserServiceImpl.*(..))")
public void finallyAOP() {
    System.out.println("最终通知已执行");
}

这个过于简单便不演示了,可自行测试,最终通知的执行时机在环绕通知结束之前

@Pointcut 定义切入点别名

当通知增强方法使用的切入点表达式相同时,编写和维护比较麻烦,可以通过@Pointcut定义一个表达式别名,后面使用该表达式时只需要写在execution属性写入方法名即可,一般带@Pointcut的修饰符用private,因为没有别的实际作用

// 使用别名
@After("aliasCut()")
public void finallyAOP() {
    System.out.println("最终通知已执行");
}

// 定义别名
@Pointcut("execution(* *..UserServiceImpl.*(..))")
private void aliasCut() {
}

总体而言,AspectJ功能比Spring AOP更加强大和易用
完 :)