嘘~ 正在从服务器偷取页面 . . .

Spring Aop


Spring

官网:https://spring.io/

文档直达:Spring 文档

一般,Spring 是 Spring Framework 的简称。

Spring 是干嘛的?

Spring可以让对象与对象(模块与模块)之间的关系不在通过硬编码关联,而是通过配置类或注解标记进行管理。
Spring是一个容器。

2.Spring AOP

AOP全称:Aspect-Oriented Programming,面向切面编程。

AOP 也是Spring 非常实用的核心组件。

AOP可以为某一类对象进行控制,实现方法增加,在调用该类对象的具体方法的前后去调用其他需要使用模块,从而达到对原有对象功能扩充目的。

Spring AOP是基于代理的框架,支持2种代理模式:

  • 使用JDK动态代理。JDK动态代理内置于JDK中。

  • CGLIB为给定的目标对象创建代理的动态代理。CGLIB是一个通用的开源类定义库,打包在spring-core包中。

如果要代理的目标对象至少实现了一个接口,则使用JDK动态代理。目标类型实现的所有接口都是代理的。如果目标对象没有实现任何接口,则会创建一个CGLIB代理。

强制使用CGLIB注意事项:
(1)对于CGLIB,final方法不能被通知,因为它们不能在运行时生成的子类中被重写
(2)Spring 4.0 之后,代理对象的构造函数不再被调用两次,因为CGLIB代理实例是通过objensis创建的。只有用户的JVM不允许构造函数绕过时,才会看到来自Spring的AOP支持的双重调用和相应的调试日志条目。

开启强制cglib:

<aop:config proxy-target-class="true">
    <!-- other beans defined here... -->
</aop:config>

proxy-target-class="true"可以用在<tx:annotation-driven/><aop:aspectj-autoproxy/>, <aop:config/>三个标签上开启强制CGLIB动态代理。

如果使用@AspectJ自动代理支持:

<aop:aspectj-autoproxy proxy-target-class="true"/>

AOP启用与声明

  1. AOP @Aspectj的启用
<aop:aspectj-autoproxy/>

或者:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}
  1. AOP切面声明

AOP切面声明方式有两种:XML方式注解方式

  • XML方式:

    <bean id="myLogAspect" class="com.xiaocai.MyLogAspect">
      <!-- configure properties of the aspect here -->
    </bean>
  • 注解声明:

@Aspect
public class MyLogAspect {

}

切入点支持

切入点声明表达式支持:

  • execution :用于匹配方法执行连接点。指定的连接点范围包或类。

  • within:使用within(类型表达式)。匹配指定类型内的方法执行;

  • this:常用于绑定形式。使用this(类型全限定名)匹配当前AOP代理对象类型的执行方法,注意是AOP代理对象的类型匹配,这样就可能包括引入接口方法也可以匹配;注意this中使用的表达式必须是类型全限定名,不支持通配符;

  • target:使用target(类型全限定名)。匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;注意target中使用的表达式必须是类型全限定名,不支持通配符;

  • args:使用args(参数类型列表)。匹配当前执行的方法传入的参数为指定类型的执行方法;注意是匹配传入的参数类型,不是匹配方法签名的参数类型;参数类型列表中的参数必须是类型全限定名,通配符不支持。

  • @target:使用@target(全限定类型名称的注解)。匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解。注解类型也必须是全限定类型名;

  • @args:使用@args(全限定类型名称的注解)。匹配当前执行的方法传入的参数持有指定注解的执行;注解类型也必须是全限定类型名;

  • @within:使用@within(全限定类型名称的注解)。匹配所有持有指定注解类型内的方法;注解类型也必须是全限定类型名。

  • @annotation:使用@annotation(全限定类型名称的注解)。匹配当前执行方法持有指定注解的方法;注解类型也必须是全限定类型名。

  • bean:匹配特定命名的bean或者带通配符的一组bean。 Spring aop环境特有的。

类型匹配语法符号:

  • * :匹配任何数量字符;
  • ..:匹配任何数量字符的重复,如在类型模式中匹配任意数量子包,在方法参数模式中匹配任意数量参数。
  • + :匹配指定类型的子类型;仅能作为后缀放在类型模式后边。

找点栗子说明:

  • bean 的示例

先看bean,比如将指定的bean作为切入点,参数可以是bean的id或者名字。

    @Pointcut("bean(userService)")
    public void beanPoint(){

    };

bean还支持通配符(*)和逻辑符 &&, ||!

如匹配所有的Service切入:

    @Pointcut("bean(*Service)")
    public void beanPoint(){

    };

如匹配所有非Dao切入:

    @Pointcut("bean(!loginDao)")
    public void beanNotLoginDaoPoint(){

    }
  • execution 示例

    • 匹配service 包下的任意无参的方法切入:

      @Pointcut("execution(* cn.xiaocai.*.service+.*())")
      private void anyNoParamMethodUnderPackage() {} 
    • 匹配 service 包下的任意只有一个参数的方法执行切入:

      @Pointcut("execution(* cn.xiaocai.*.service.*(*))")
      private void anyOneParamMethodUnderPackage() {} 
    • 匹配 service 包下的不限定参数的任意方法执行切入:

      @Pointcut("execution(* cn.xiaocai.*.service.*(..))")
      private void anyMethodUnderPackage() {} 
    • 匹配 service 包下的限定只有一个参数且参数类型为java.util.Date的方法执行切入:

      @Pointcut("execution(* cn.xiaocai.*.service.*(java.util.Date))")
      private void givenParamaterType() {} 
    • 匹配 service 包下的不限定参数的任意方法且抛出IllegalArgumentExceptionArrayIndexOutOfBoundsException异常执行切入:

      @Pointcut("execution(* cn.xiaocai.*.service.*(..)) throws IllegalArgumentException, ArrayIndexOutOfBoundsException")
      private void anyPackageWithException() {} 
    • 匹配 任意路径下只有一个参数且参数声明(持有)@Param注解的任意方法执行切入:

      @Pointcut("execution(* *(@Param *)")
      private void oneParamAnnotation() {} 
    • 匹配所有被@MyLog注解修饰的任意方法:

      @Pointcut("@cn.xiaocai.annotation.MyLog * *(..)")
      private void myLogAnnotation() {} 
    • 匹配任何被@MyLogRequestMapping注解修饰的方法:

      @Pointcut("@cn.xiaocai.annotation.MyLog @org.springframework.web.bind.annotation.RequestMapping  * *(..)")
      private void manyAnnotation() {} 
    • 匹配任何被@BizEmailBizWarn注解修饰的方法:

      @Pointcut("@(cn.xiaocai.annotation.BizEmail || cn.xiaocai.annotation.BizWarn) * *(..)")
      private void manyAnnotation() {} 
    • 匹配 任意路径下参数声明@MyAnnotation注解并且参数类型上也有@MyAnnotation注解的任意方法:

      @Pointcut("* *(@MyAnnotation (@cn.xiaocai.annotation.MyAnnotation *), @cn.xiaocai.annotation.MyAnnotation (@cn.javass..MyAnnotation *))")
      private void manyParamsAnnotation() {} 
    • 任意公共方法执行切入:

      @Pointcut("execution(public * *(..))")
      private void anyPublicOperation() {} 
    • 任意update开头的方法切入:

      @Pointcut("execution(* update*(..))")
      private void anyUpdateMethodOperation() {} 
    • AccountService接口的任意方法切入:

      @Pointcut("execution(* com.xyz.service.AccountService.*(..))")
      private void givenInterfaceOperation() {} 
    • com.xyz.service包及子包下的任意方法切入:

      @Pointcut("execution(* com.xyz.service.*.*(..))")
      private void givenPackageOperation() {} 
    • execution 组合引用匹配的示例:

      @Aspect
      public class CombinePointcuts{
      
      // 匹配任意public方法作为切入点
      @Pointcut("execution(public * *(..))")
      private void anyPublicOperation() {} 
      
      //匹配com.xyz.myapp.trading 包下的方法执行作为切入点。方法执行在交易模块中,则匹配切入
      @Pointcut("within(com.xyz.myapp.trading..*)")
      private void inTrading() {
      
      } 
      
      // 组合引用以上两者使用,匹配交易模块中的任意public方法
      @Pointcut("anyPublicOperation() && inTrading()")
      private void tradingOperation() {}
      }

      最好的做法是使用较小的命名组件构建更复杂的切入点表达式。

  • within 示例
    • 指定包下的类任意连接点:
      @Pointcut("within(com.xyz.service.*)")
      private void givenService() {}
    • 匹配指定包及子包下的BizPointcutService类型及子类型的任意方法:
      @Pointcut("within(com.xyz.service.BizPointcutService+)")
      private void givenServiceAndSub() {}
    • 匹配目标对象的声明类型持有@TTransactional注解:
      @Pointcut("@within(org.springframework.transaction.annotation.Transactional)")
      private void anyTargethasAnnotation() {}  
  • this 示例

    • Aop代理实现AccountService接口的任何连接点

      @Pointcut("this(com.xyz.service.AccountService)")
      private void givenServiceProxyPoint() {}
    • 声明注解的代理连接

      @Aspect
      public class UsageTracking {
      
      @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
      public static UsageTracked mixin;
      
      @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
      public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
      }
      }

      这里使用@DeclareParents引入UsageTracked接口声明,就是引入接口使用接口的方法。而所有的Bean都实现了这个UsageTracked接口。每次调用公共切入的businessService() 方法时会自动找到对应的bean。

  • target 示例

所有实现了AccountService接口的实现类的目标对象切入连接点(针对接口):

@Pointcut("target(com.xyz.service.AccountService)")
private void anyTargetInterfaceImpl() {}  

特定实现类的目标对象切入连接点(针对具体的类):

@Pointcut("target(com.xyz.service.impl.AccountServiceImpl)")
private void anyTargetImplClass() {}  
  • args 示例

args 匹配当前执行的方法传入的参数为指定类型的执行方法;注意是匹配传入的参数类型,不是匹配方法签名的参数类型;参数类型列表中的参数必须是类型全限定名,通配符不支持;args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用;。

匹配只有一个参数且参数类型为 java.io.Serializable方法切入:

@Pointcut("args(java.io.Serializable)")
private void oneParamsForSerialized() {}  

匹配第一个参数的类型为 java.io.Serializable,后面跟任意个数任意类型的参数的方法切入:

@Pointcut("args(java.io.Serializable,..)")
private void anyParamsForMany() {}  
  • @target 示例

目标对象持有@Transactional注解的

@Pointcut("@target(org.springframework.transaction.annotation.Transactional)")
private void anyTargethasAnnotation() {}  
  • @annotation 示例

持有@Transactional注解的执行方法:

@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
private void anyMethodHasAnnotation() {}  
  • 共享公共切入点的示例:
package com.xyz.myapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class CommonPointcuts {

    /**
     * 切入方法在 com.xyz.myapp.web 包及子包下的类定义的方法
     */
    @Pointcut("within(com.xyz.myapp.web..*)")
    public void inWebLayer() {}

    /**
     * 切入方法在 com.xyz.myapp.service 包及子包下的类定义的方法
     */
    @Pointcut("within(com.xyz.myapp.service..*)")
    public void inServiceLayer() {}

    /**
     * 切入点在 com.xyz.myapp.dao 包及子包下的类定义的方法
     */
    @Pointcut("within(com.xyz.myapp.dao..*)")
    public void inDataAccessLayer() {}

    /**
     * 切入方法在 业务执行方法 com.xyz.myapp..service 包及子包的实现类中,接口声明模式
     * 相当于bean(*Service) 
     */
    @Pointcut("execution(* com.xyz.myapp..service.*.*(..))")
    public void businessService() {}

    /**
     *
    * 切入方法在 业务执行方法 com.xyz.myapp.dao 包及子包的实现类中,接口声明模式
     */
    @Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
    public void dataAccessOperation() {}

}

切入通知

通知声明

通知与切入点表达式相关联,并在与切入点匹配的方法执行之前、之后或周围运行。切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。

几种通知形式:

  • @Before:前置通知。
  • @After:后置通知、最终通知。
  • @AfterReturning:返回后通知。
  • @AfterThrowing:抛出异常通知。目标方法抛出异常时执行。
  • @Around:环绕通知。

@Before

前置通知

  • 引用已经命名的切入点:

    @Aspect
    public class MyLogAspect {
    
      @Pointcut("@annotation(com.xiaocai.base.demo.annotation.MyLog)")
      public void logPoint(){};
    
      @Before(value = "logPoint()")
      public void logBefore(){
    
      }
    }
  • 就地声明切入点:

    @Aspect
    public class MyLogAspect {
    
      @Before(value = "@annotation(com.xiaocai.base.demo.annotation.MyLog)")
      public void logBefore(){
      }
    }

    其他同理。

@After

类似try-catch-finally结构里的finally块。

@Aspect
public class MyLogAspect {
    @Pointcut("@annotation(com.xiaocai.base.demo.annotation.MyLog)")
    public void logPoint(){};

    @After(value = "logPoint()")
    public void logAfter(){
        log.info("--log After ");

    }
}

@AfterReturning 返回后通知

@AfterReturning的返回参数绑定到通知参数上。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class MyLogAspect {

    @AfterReturning(
        pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }
}

该通知只能从连接点(用户声明的目标方法)本身接收异常。

@Around 环绕通知

  • 基本示例
@Aspect
public class MyLogAspect {

    @Pointcut("@annotation(com.xiaocai.base.demo.annotation.MyLog)")
    public void logPoint(){};

    @Around(value = "logPoint()")
    public Object logAround(ProceedingJoinPoint joinPoint){
        log.info("--log  Around  Before --");
        // 获取方法名称
        String methodName = joinPoint.getSignature().getName();
        // 获取入参
        Object[] param = joinPoint.getArgs();
        log.info("--methodName {},  param {}", methodName, param);
        // 继续执行方法
        Object retVal = 
        try {
            retVal = joinPoint.proceed();

        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        log.info("--log  Around  After --");
        return retVal ;
    }
}

环绕通知的第一个参数必须是ProceedingJoinPoint类型。返回类型可以声明为voidproceed()方法最多只能调用一次,可以不调用。

  • 访问当前连接点
    比较实用的方法:
    • getArgs():返回方法参数。
    • getThis():返回代理对象。
    • getTarget():返回目标对象。
    • getSignature():返回所建议的方法的说明。
    • toString():打印建议的方法的有用描述。

JoinPoint接口源码:

package org.aspectj.lang;  
import org.aspectj.lang.reflect.SourceLocation;  
public interface JoinPoint {  
    String toString();         //连接点所在位置的相关信息  
    String toShortString();     //返回连接点的缩写字符串表示形式。
    String toLongString();     //返回连接点的扩展字符串表示形式
    Object getThis();         //返回AOP代理对象 ,返回当前正在执行的对象。
    Object getTarget();       //返回目标对象  
    Object[] getArgs();       //返回被通知方法参数列表  
    Signature getSignature();  //返回当前连接点签名  
    SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置  
    String getKind();        //连接点类型  
    StaticPart getStaticPart(); //返回一个封装此连接点的静态部分的对象
} 

通知参数传递

使用args表达式进行参数绑定。

@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}

或者:

@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}

args(account,..) 的作用有两个:一是只匹配那些方法至少接受一个参数的方法执行;二是通过Account参数使通知可以使用实际的Account对象。

代理对象(this)、目标对象(target)和注释(@within、@target、@annotation和@args)都可以以类似的方式绑定。

比如我们自己定义一个注解,然后传递注解:

我的注解:

/**
 * @author Xiaocai.Zhang
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface LimitChecked {
    String name() default "";
}

切面与通知:

/**
 * @author Xiaocai.Zhang
 */
@Slf4j
@Aspect
@Component
public class LimitCheckedAspect {
    @Pointcut("@annotation(com.xiaocai.base.demo.annotation.LimitChecked)")
    public void checkedPoint(){};

    @Before(value = "checkedPoint() && @annotation(limitChecked)")
    public void checkBefore(LimitChecked limitChecked){
        String nameKey = limitChecked.name();
    }

    @Around(value = "checkedPoint() && @annotation(limitChecked)")
    public CommonResult checkAround(ProceedingJoinPoint joinPoint, LimitChecked limitChecked){
        String nameKey = limitChecked.name();
    }
}

这样在前置通知或环绕通知都可以拿到对应注解的属性值。

通知参数与泛型

Spring Aop 可以处理类声明和方法参数中使用的泛型。

声明接口:

public interface Sample<T> {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection<T> param);
}

通知使用:

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}

通知参数命名

如果用户已明确指定参数名,则使用指定的参数名。通知和@Pointcut注解都有一个可选的argNames属性,可以使用它来指定带注释方法的参数名。

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code and bean
}

如果第一个参数是JoinPoint、ProceedingJoinPoint或JoinPoint.StaticPart类型,则可以从argNames属性的值中省略参数的名称。

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code, bean, and jp
}

不需要收集连接点上下文的,可以省略argNames属性。

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}

多个切点或切面执行先后

(1)实现 org.springframework.core.Ordered 接口
(2)使用@Order注解。
(3)数字越小,优先级越高。

在切入点添加注解:

    @Order(1) // Order 代表优先级,数字越小优先级越高
    @Pointcut("@annotation(com.xiaocai.base.demo.annotation.LimitChecked)")
    public void checkedPoint(){};

在通知添加注解:

一般是针对同一个切入点出现多个切面或多个通知操作时。

    @Order(1) 
    @Before(value = "logPoint()")
    public void logBefore(){
        log.info("--log before ");

    }

切面实例化模型

默认情况下,应用程序上下文中每个切面都有一个实例。AspectJ称之为单例实例化模型。
Spring 支持 AspectJ的 perthispertarget 的实例化。

@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
public class MyAspect {

    private int someState;

    @Before("com.xyz.myapp.CommonPointcuts.businessService()")
    public void recordServiceUsage() {
        // ...
    }
}

perthis子句的作用是为执行businessService()的每个唯一服务对象创建一个切面实例。每个唯一的对象会在与切入点表达式匹配的连接点处绑定到该对象。

切面实例是在服务对象第一次调用方法时创建的,当服务对象不在生命周期范围内时,切面也无法进入声明周期范围。所以在创建切面实例之前,其中的任何通知都不会执行,但是只要创建了切面实例,其中声明的通知就可以在匹配的连接点上执行,当然前提是服务对象与之相关联了。

pertarget实例化模型的工作方式与perthis完全相同,但它在匹配的连接点为每个唯一的目标对象创建一个方面实例。

总结:perthis 和 pertarget 都可以进行切面实例化,但是perthis是单例模式的实例化,而pertarget是原型模式的实例化。

AOP Demo

业务执行有时会由于并发问题而失败(例如,死锁失败者)。

如果重试该操作,则下次尝试可能会成功,对于这种适合重试的业务服务,可以定义一个切面去实现重试操作:

@Aspect
public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }
}

该切面实现了有序接口Ordered,以便可以将切面的优先级设置为高于事务通知,因为每次重试时都需要一个新的事务。maxRetriesorder属性都由Spring配置。主要重试操作逻辑发生在doconcurrentooperation()方法中。请注意,这里将重试逻辑应用于每个businessService()方法。 如果由于悲观的LockingFailureException而失败,我们将重试,知道达到我们设置的重试次数。

对应的配置:

<aop:aspectj-autoproxy/>

<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
    <property name="maxRetries" value="3"/>
    <property name="order" value="100"/>
</bean>

要优化切面使其只重试幂等操作,定义一个幂等注解

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

然后使用@Idempotent注解来注释服务操作的实现。

优化匹配连接点:

@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    // ...
}

XML AOP

XML模式用的少,暂时就先不整理了。

Spring中使用AspectJ进行加载时织入

加载时织入,LoadTimeWeaving,简称LTW。

AOP通过为目标类织入切面的方式,实现对目标类功能的增强。按切面被织如到目标类中的时间划分,主要有3种:

  • 运行时织入
    这是最常见的,比如在运行期通过为目标类生成动态代理的方式实现AOP就属于运行期织入,这也是Spring AOP中的默认实现,Spring也提供了两种创建动态代理的方式:自带的JDK针对接口的动态代理和使用CGLib动态创建子类的方式创建动态代理。

  • 编译时织入
    使用特殊的编译器在编译期将切面织入目标类,这种比较少见,因为需要特殊的编译器的支持。

  • 加载时织入

类加载时织入通过字节码编辑技术在类加载期将切面织入目标类中,它的核心思想是在目标类的class文件被JVM加载前,通过自定义类加载器或者类文件转换器将横切逻辑织入到目标类的class文件中,然后将修改后class文件交给JVM加载。这种织入方式可以简称为LTW(LoadTimeWeaving)。

Spring的LTW支持中的关键组件是LoadTimeWeaver接口(在org.springframework.instrument.classloading包)。LoadTimeWeaver接口源码:

package org.springframework.instrument.classloading;

import java.lang.instrument.ClassFileTransformer;

public interface LoadTimeWeaver {
    void addTransformer(ClassFileTransformer var1);

    ClassLoader getInstrumentableClassLoader();

    ClassLoader getThrowawayClassLoader();
}

基本原理:

Spring LTW通过读取classpath下META-INF/aop.xml文件,获取切面类和要被切面织入的目标类的相关信息,再通过LoadTimeWeaverClassLoader加载类文件时将切面织入目标类中。

Spring中可以通过LoadTimeWeaver将Spring提供的ClassFileTransformer注册到ClassLoader类加载器中。在类加载时,注册的ClassFileTransformer读取类路径下META-INF/aop.xml文件中定义的切面类和目标类信息,在目标类的class文件真正被VM加载前织入切面信息,生成新的Class文件字节码,然后交给VM加载,然后创建目标类的实例,实现AOP功能。

spring支持需要2个包。

  • spring-aop.jar
  • aspectjweaver.jar
  • spring-instrument.jar(如果使用Spring提供的代理来启用检测)

官方Demo

1、首先声明切入点和切面

package foo;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StopWatch;
import org.springframework.core.annotation.Order;

@Aspect
public class ProfilingAspect {

    @Around("methodsToBeProfiled()")
    public Object profile(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch sw = new StopWatch(getClass().getSimpleName());
        try {
            sw.start(pjp.getSignature().getName());
            return pjp.proceed();
        } finally {
            sw.stop();
            System.out.println(sw.prettyPrint());
        }
    }

    @Pointcut("execution(public * foo..*.*(..))")
    public void methodsToBeProfiled(){}
}

2、创建创一个META-INF/aop.xml文件

创建创META-INF/aop.xml文件用于通知AspectJ weaver将我们的切面ProfilingAspect编织到类中。

aop.xml 配置:

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
    <weaver>
        <!-- 要织入切面的目标类 这里使用的是包,也可以明确指定某个类 foo.StubEntitlementCalculationService  -->
        <include within="foo.*"/>
    </weaver>

    <aspects>
        <!-- 要织入的切面 -->
        <aspect name="foo.ProfilingAspect"/>
    </aspects>
</aspectj>

3、开启LTW支持

Spring 的XML配置开启LTW.

<?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 要织入的模板 service -->
    <bean id="entitlementCalculationService"
            class="foo.StubEntitlementCalculationService"/>

    <!-- 开启LTW支持 -->
    <context:load-time-weaver/>
</beans>

如果是在 SpringBoot 中可以使用@EnableLoadTimeWeaving注解开启LTW支持。

@Configuration
@ComponentScan("com.xiaocai.ltw.*")
@EnableLoadTimeWeaving(aspectjWeaving=AUTODETECT)
public class LtwAopConfig{
}

4、测试LTW

package foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public final class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class);

        EntitlementCalculationService entitlementCalculationService =
                (EntitlementCalculationService) ctx.getBean("entitlementCalculationService");

        // 验证切面织入
        entitlementCalculationService.calculateEntitlement();
    }
}

5、使用代理开启

可以直接使用java代理来执行main方法:

java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main

或者配置JVM运行参数:

-javaagent:C:/projects/foo/lib/global/spring-instrument.jar

如果是 springboot 可以引入对应的jar包组件配置

        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <agent>
                    ${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar
                </agent>
                <agent>
                    ${settings.localRepository}/org/springframework/spring-instrument/${spring.version}/spring-instrument-${spring.version}.jar
                </agent>
            </configuration>
        </plugin>

对于独立的java应用程序,spring提供了一个InstrumentationLoadTimeWeaver,需要一个通用的JVM代理spring-instrument.jar。由@EnableLoadTimeWeaving<context:load-time-weaver/>的配置完成自动检测。同时必须配置jvm参数:

-javaagent:/path/to/spring-instrument.jar

在上面的配置中,如果要指定特定的LoadTimeWeaver可以进行配置。

  • 使用Java配置:

指定特定的LoadTimeWeaver需要实现LoadTimeWeavingConfigurer接口并重写getLoadTimeWeaver()方法

@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {

    @Override
    public LoadTimeWeaver getLoadTimeWeaver() {
        return new ReflectiveLoadTimeWeaver();
    }
}
  • 使用XML配置:

直接配置对应的weaver-class即可。

<?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:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:load-time-weaver
            weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>

</beans>


版权声明: 本博客所有文章除特別声明外,均采用 CC BY-SA 4.0 许可协议。转载请注明来源 Small-Rose / 张小菜 !
评论
  目录