`

新版Spring Aop配置方式

阅读更多

前言

 

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类中的所有方法:



 

 

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

  • 大小: 6.3 KB
2
1
分享到:
评论
4 楼 masuweng 2017-06-28  
     
3 楼 niedj 2017-06-27  
好文章,学习了
2 楼 moon_walker 2017-06-27  
masuweng 写道
多谢了好文章

谢谢
1 楼 masuweng 2017-06-27  
多谢了好文章

相关推荐

Global site tag (gtag.js) - Google Analytics