欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

新版Spring Aop配置方式

程序员文章站 2022-05-03 13:53:14
...

前言

 

Spring aop技术,个人理解 主要解决代码复用,避免重复性编写类似代码问题。比较典型的三种场景就是 日志打印、权限验证、事务处理。其实远不至于这三种场景,在编码过程中如果发现某些类似的代码频繁的出现在各个方法中,就可以考虑是否可以用aop统一进行处理,而不是在每个方法都进行一次。

 

Spring aop相关术语

 

连接点:判断是否需要使用spring aop技术,首先提取某一类的业务方法进行分析,所有这些方法就是连接点。

 

切点:进一步在所有的连接点中进行分析,提取出需要进行统一处理的方法,是连接点的子集。解决 where的问题,主要通过切点表达式进行过滤,如典型的配置方式execution(* com.xxx.xxx.*(..))

 

通知:简单的说,就是首先从切点中提取出来的共同操作:以前这些操作分布在各个方法体中,现在提取到同一个类中统一管理,解决how(执行什么)的问题; Spring aop中定义了5种通知,解决when(什么时候执行)的问题,根据自己的业务场景选择使用:

前置通知(Before):在目标方法执行前,首先调用该方法。

后置通知(After):在目标方法执行完成后,再调用该方法。不管是目标方法执行成功,还是抛出异常,都会调用。

返回通知(AfterReturning):在目标方法执行成功后,再调用该方法。

异常通知(AfterThrowing):在目标方法执行抛出异常后,调用该方法。

环绕通知(Around):对目标方法进行包裹,理论上可以在环绕通知里,实现上述4种通知。

 

切面:用于承载 通知+切点的类。把wherewhen and how (在哪儿执行、什么时候执行、执行什么)执行整合在一起。

 

引入:向现有的目标类添加新的方法和属性,只需要在切面中添加即可。反过来讲,也可以把目标类*同的属性和方法抽取到切面中,方便统一管理。

 

织入:是把切面应用到目标对象并创建新的代理对象的过程。创建代理对象又分为静态代理 和动态代理,使用AspectJ是静态代理,在编译期或者类加载器创建代理对象;使用spring aop是动态代理,在运行期动态的为目标对象创建代理对象。

 

Spring aop支持jdk动态代理和CGLIB动态代理。默认情况下,如果目标对象实现了接口,采用JDK的动态代理实现AOP,否则使用CGLIB动态代理。当然也可以通过配置指定。

 

本章主要讲解spring支持的三种aop使用方式(本章是基于spring4.3进行讲解,对于spring3.0以前的版本,还请使用经典方式,这里不再讲解):

1、基于注解方式(使用AspectJ注解,但本质上还是基于动态代理,这里只是用到AspectJ注解)。

2、基于xml配置方法。

3、注入AspectJ切面的静态代理方式。

 

spring中我们通常使用前两种方式(这里暂且称之为spring aop),第三种方式作为补充(这里暂且称之为 AspectJ aop)。Spring aop只能支持在普通方法上织入切面,但无法使用在构造方法上。主要原因 前面也提到过,spring aop是在运行期动态的创建代理对象,此时目标对象创建完成,不会再有构造方法的调用。作为补充,这种情况下只能使用AspectJ静态代理,在编译期货类加载期就进行代理对象的创建。

 

下面分别对三种切面注入方式进行讲解。

 

基于注解方式

 

spring3.0开始,就比较推崇用注解代替xml配置,所以我们首先对基于注解方式的spring aop用法进行讲解,首先看下切面类:

 

/**
 * 统一日志aop
 */
@Component  //标记为一个
@Aspect //标记为切面
public class LogAop {
    private static final Log log = LogFactory.getLog(LogAop.class);
 
    //定义切点 方便复用
    @Pointcut("execution(* com.sky.aop.service.*.*.*(..))")
    public void log(){};
 
    //前置通知
    @Before("log()")
    public void beforeLog(JoinPoint jp){
        log.info(jp.getSignature().getDeclaringTypeName()+"类的"+jp.getSignature().getName()+ "方法Before日志");
    }
 
    //环绕通知
    @Around("log()")
    public void aroundLog(ProceedingJoinPoint jp) {
        try {
            log.info(jp.getSignature().getDeclaringTypeName()+"类的"+jp.getSignature().getName()+"方法Around通知开始");
            jp.proceed();
            log.info(jp.getSignature().getDeclaringTypeName() + "方法Around通知结束");
        }catch (Throwable throwable) {
            Object[] args = jp.getArgs();
 
            System.out.println("参数列表值为:");
            for (Object one: args){
                log.error(one.toString());
            }
            log.error(jp.getSignature().getDeclaringTypeName() + "类的" + jp.getSignature().getName() + "调用异常", throwable);
 
        }
 
    }
 
    //后置通知
    @After("log()")
    public void after(JoinPoint jp){
        log.info(jp.getSignature().getDeclaringTypeName()+"类的"+jp.getSignature().getName()+  "方法after日志");
    }
 
    //返回通知
    @AfterReturning("log()")
    public void afterRet(JoinPoint jp){
        log.info(jp.getSignature().getDeclaringTypeName()+"类的"+jp.getSignature().getName()+ "方法AfterReturning日志");
    }
 
    //异常通知
    @AfterThrowing("log()")
    public void afterError(JoinPoint jp){
        log.info(jp.getSignature().getDeclaringTypeName()+"类的"+jp.getSignature().getName()+ "方法AfterThrowing日志");
    }
}
 

 

该类模拟的是统一的日志打印,我们业务场景通常为:在目标方法调用之前、之后、或异常时需要进行日志打印,LogAop切面类定义spring aop支持的5种通知类型(前面已经讲解),分别对应5类注解:@Before@Around@After@AfterReturning@AfterThrowing

 

再来看下切点定义:

//定义切点 方便复用
    @Pointcut("execution(* com.sky.aop.service.*.*.*(..))")
public void log(){};

 

log()方法表示切点,方法体为空,只是做切点标记使用。

execution(* com.sky.aop.service.*.*.*(..) 表达式:

第一个*:表示返回任意类型的方法。

com.sky.aop.service:表示包路径。

第二个*:表示com.sky.aop.service包下所有的子包(不包含子包的子包)。

第三个*: 表示子包下的任意类。

第四个*: 表类里的任意方法。

(..): 表示任意参数的方法。

 

本示例中,会匹配到下列ProductServiceUserService类中的所有方法:


新版Spring Aop配置方式
            
    
    博客分类: spring spring aopAspectJ 
 

 

ProductServiceUserService类都是接口类,其实现类在impl包下,这时spring aop会默认使用 JDK动态代理。这里只以ProductService为例,代码为:

public interface ProductService {
    void add(int id);
}

再看下其实现类ProductServiceImpl的代码:

@Component
public class ProductServiceImpl implements ProductService{
    @Override
    public void add(int id) {
        System.out.println("ProductService的add方法调用,参数为:"+id);
    }
}
 

 

至此,统一日志添加的coding工作已经完成,可以看到业务实现类ProductServiceImpl跟普通spring bean没有任何区别。实际上只是多了一个切面类LogAop

 

下面我们使用Junit进行单元测试,看看效果,首先创建spring bean自动装配类SpringConfig,代码如下:

@ComponentScan(basePackages = "com.sky.aop")
@EnableAspectJAutoProxy
public class SpringConfig {
}

 

@EnableAspectJAutoProxy 注解的作用为: 启动自动代理。

 

最后再来看下Junit单元测试类:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=SpringConfig.class)
public class SpringAopTest {
 
    @Autowired
    private ProductService productService;
 
    @Test
    public void productTest(){
        productService.add(1);
    }
}

 

 

执行测试方法productTest,打印信息如下:

六月 26, 2017 4:58:30 下午 com.sky.aop.LogAop aroundLog
信息: com.sky.aop.service.product.ProductService类的add方法Around通知开始
ProductService的add方法调用,参数为:1
六月 26, 2017 4:58:30 下午 com.sky.aop.LogAop beforeLog
信息: com.sky.aop.service.product.ProductService类的add方法Before日志
六月 26, 2017 4:58:30 下午 com.sky.aop.LogAop aroundLog
信息: com.sky.aop.service.product.ProductService方法Around通知结束
六月 26, 2017 4:58:30 下午 com.sky.aop.LogAop after
信息: com.sky.aop.service.product.ProductService类的add方法after日志
六月 26, 2017 4:58:30 下午 com.sky.aop.LogAop afterRet
信息: com.sky.aop.service.product.ProductService类的add方法AfterReturning日志
 

 

这里没有异常抛出,所以只有“异常通知”未执行,其余通知均已执行。测试通过。

 

基于xml配置方法

 

业务类不变,为了区分,这里新建一个切面类LogXmlAop,通知方法与上述的LogAop切面类相同,只是去打掉了相关注解,内容如下:

@Component
public class LogXmlAop {
    private static final Log log = LogFactory.getLog(LogAop.class);
 
    //前置通知
    public void beforeLog(JoinPoint jp){
        log.info(jp.getSignature().getDeclaringTypeName()+"类的"+jp.getSignature().getName()+ "方法Before日志");
    }
 
    //环绕通知
    public void aroundLog(ProceedingJoinPoint jp) {
        try {
            log.info(jp.getSignature().getDeclaringTypeName()+"类的"+jp.getSignature().getName()+"方法Around通知开始");
            jp.proceed();
            log.info(jp.getSignature().getDeclaringTypeName() + "方法Around通知结束");
        }catch (Throwable throwable) {
            Object[] args = jp.getArgs();
 
            System.out.println("参数列表值为:");
            for (Object one: args){
                log.error(one.toString());
            }
            log.error(jp.getSignature().getDeclaringTypeName() + "类的" + jp.getSignature().getName() + "调用异常", throwable);
 
        }
 
    }
 
    //后置通知
    public void after(JoinPoint jp){
        log.info(jp.getSignature().getDeclaringTypeName()+"类的"+jp.getSignature().getName()+  "方法after日志");
    }
 
    //返回通知
    public void afterRet(JoinPoint jp){
        log.info(jp.getSignature().getDeclaringTypeName()+"类的"+jp.getSignature().getName()+ "方法AfterReturning日志");
    }
 
    //异常通知
    public void afterError(JoinPoint jp){
        log.info(jp.getSignature().getDeclaringTypeName()+"类的"+jp.getSignature().getName()+ "方法AfterThrowing日志");
    }
}
 

 

可以看到这个类就是一个普通的bean 类,我们通过xml配置可以把这个普通的bean类定义为一个切面类,在classpath下新增配置文件spring-aop.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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                            http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
 
    <context:component-scan base-package="com.sky.aop" />
 
    <aop:config>
        <aop:aspect ref="logXmlAop">
            <aop:pointcut id="log" expression="execution(* com.sky.aop.service.*.*.*(..))"/>
            <aop:before pointcut-ref="log" method="beforeLog"/>
            <aop:around pointcut-ref="log" method="aroundLog"/>
            <aop:after pointcut-ref="log" method="after" />
            <aop:after-returning pointcut-ref="log" method="afterRet" />
            <aop:after-throwing pointcut-ref="log" method="afterError" />
        </aop:aspect>
    </aop:config>
</beans>
 

 

可以看到与基于注解的方式差不多,标记切面、定义切点、定义通知:

aop:config:表示该包裹体内部 spring aop配置;

aop:aspect:定义切面,ref表示引用的spring bean id,这里引用的自动装配beanLogXmlAop的实例;

aop:pointcut:定义切点,以便定义通知时复用;

aop:before:前置通知

aop:around:环绕通知

aop:after:后置通知

aop:after-returning:返回通知

aop:after-throwing:异常通知

 

至此,基于“基于xml配置方法”的spring aop实现方式已经完成,主要工作就是通过xml配置把一个普通bean变成一个切面。

 

下面我们重新编写一个Junit测试类,通过该xml配置文件注入bean,进行测试,代码如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-aop.xml")
public class SpringAopXmlTest {
 
    @Autowired
    private ProductService productService;
 
    @Test
    public void productTest(){
        productService.add(1);
    }
}

 

 

可以看到内容基本与基于注解方法的测试类相同,只是把@ContextConfiguration注解的参数改为xml配置文件,执行测试方法,打印结果如下:

信息: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
ProductService的add方法调用,参数为:1
六月 26, 2017 5:15:39 下午 com.sky.aop.LogAop beforeLog
信息: com.sky.aop.service.product.ProductService类的add方法Before日志
六月 26, 2017 5:15:39 下午 com.sky.aop.LogAop aroundLog
信息: com.sky.aop.service.product.ProductService类的add方法Around通知开始
六月 26, 2017 5:15:39 下午 com.sky.aop.LogAop aroundLog
信息: com.sky.aop.service.product.ProductService方法Around通知结束
六月 26, 2017 5:15:39 下午 com.sky.aop.LogAop after
信息: com.sky.aop.service.product.ProductService类的add方法after日志
六月 26, 2017 5:15:39 下午 com.sky.aop.LogAop afterRet
信息: com.sky.aop.service.product.ProductService类的add方法AfterReturning日志
 

 

测试通过。

 

注入AspectJ切面

 

这种方式属于静态代理方法,严格的讲这种方式不属于spring aop,但spring支持基于AspectJ静态代理的方式。使用场景:作为spring aop的补充,当需要创建 目标对象构造方法调用时的切面,此时可以使用spring注入AspectJ切面,分两步即可完成:

第一步,首先创建切面类:

public aspect MyAspectJ {
 
    private static final Log log = LogFactory.getLog(MyAspectJ.class);
 
    public MyAspectJ(){}
 
    //定义构造方法调用切面
    pointcut newCreate():execution(com.sky.aop.aspectj.impl.AspectJServiceImpl.new());
 
    Object around():newCreate(){
        log.info("调用构造方法开始");
        Object result = proceed();
        log.info("调用构造方法结束");
        return result;
    }
 
    //定义普通方法调用切面
    pointcut runLog():execution(* com.sky.aop.aspectj.impl.AspectJServiceImpl.run());
    before():runLog(){
        log.info("调用普通方法前打印日志");
    }
 
}

 

 

这里定义了两个切面:一个是目标方法是构造方法,另一个是普通方法。定义了两个通知:一个是环绕通知,一个是前置通知。由于项目中使用较少,这里不做讲解,其他具体用法参考官方文档:http://www.eclipse.org/aspectj/doc/released/progguide/index.html

 

第二步:把切面注入spring,配置如下:

<?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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
 
       <context:component-scan base-package="com.sky.aop.aspectj" />
       <bean class="com.sky.aop.MyAspectJ" factory-method="aspectOf" />
 
</beans>

 

 

我们再来看下,目标方法所在的业务类AspectJServiceImpl

@Component
public class AspectJServiceImpl implements AspectJService{
    private static final Log log = LogFactory.getLog(AspectJServiceImpl.class);
 
    public AspectJServiceImpl(){
        log.info("AspectJServiceImpl构造方法执行");
    }
 
    @Override
    public void run() {
        log.info("AspectJService run方法执行");
    }
}

 

 

注入AspectJ切面 完成,下面创建Junit单元测试,新建测试类SpringAspectJTest,内容如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-aspectj-aop.xml")
public class SpringAspectJTest {
 
    @Autowired
    private AspectJService aspectJService;
 
    @Test
    public void ajTest(){
        aspectJService.run();
    }
}

 

 

执行测试方法报错,信息如下:

java.lang.IllegalStateException: Failed to load ApplicationContext
………..省略日志..........
Caused by: java.lang.ClassNotFoundException: com.sky.aop.MyAspectJ

 

 

主要原因是因为切面类MyAspectJ不是class修饰,而是aspect修饰。普通maven编译无法编译AspectJ切面类,必须采用专用的编译器。具体Maven配置如下:

<build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>aspectj-maven-plugin</artifactId>
                <version>1.8</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <complianceLevel>1.8</complianceLevel>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
</build>

 

 

再次执行上述测试方法,打印信息如下:

信息: 调用构造方法开始
六月 26, 2017 9:01:30 下午 com.sky.aop.aspectj.impl.AspectJServiceImpl init$_aroundBody0
信息: AspectJServiceImpl构造方法执行
六月 26, 2017 9:01:30 下午 com.sky.aop.MyAspectJ init$_aroundBody1$advice
信息: 调用构造方法结束
六月 26, 2017 9:01:31 下午 com.sky.aop.MyAspectJ ajc$before$com_sky_aop_MyAspectJ$2$2f971b7a
信息: 调用普通方法前打印日志
六月 26, 2017 9:01:31 下午 com.sky.aop.aspectj.impl.AspectJServiceImpl run
信息: AspectJService run方法执行

 

测试成功。

 

至此两种常用的spring aop实现方法,以及一种spring注入AspectJ切面的方式 讲解完毕。

以上示例代码地址:https://github.com/gantianxing/spring-aop.git

  • 新版Spring Aop配置方式
            
    
    博客分类: spring spring aopAspectJ 
  • 大小: 6.3 KB
相关标签: spring aop AspectJ