新版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种通知。
切面:用于承载 通知+切点的类。把where、when 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包下所有的子包(不包含子包的子包)。
第三个*: 表示子包下的任意类。
第四个*: 表类里的任意方法。
(..): 表示任意参数的方法。
本示例中,会匹配到下列ProductService、UserService类中的所有方法:
ProductService、UserService类都是接口类,其实现类在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,这里引用的自动装配bean类LogXmlAop的实例;
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