SpringBoot系列之AOP实现的两种方式
AOP常用的实现方式有两种,一种是采用声明的方式来实现(基于XML),一种是采用注解的方式来实现(基于AspectJ)。
首先复习下AOP中一些比较重要的概念:
Joinpoint(连接点):程序执行时的某个特定的点,在Spring中就是某一个方法的执行 。
Pointcut(切点):说的通俗点,spring中AOP的切点就是指一些方法的集合,而这些方法是需要被增强、被代理的。一般都是按照一定的约定规则来表示的,如正则表达式等。切点是由一类连接点组成。
Advice(通知):还是说的通俗点,就是在指定切点上要干些什么。
Advisor(通知器):其实就是切点和通知的结合 。
一、基于XML配置的Spring AOP
采用声明的方式实现(在XML文件中配置),大致步骤为:配置文件中配置pointcut, 在java中用编写实际的aspect 类, 针对对切入点进行相关的业务处理。
业务接口:
package com.springboottime.time.service;public interface AdviceService { /*查找用户*/ public String findUser(); /*添加用户*/ public void addUser();}
业务实现:
package com.springboottime.time.service.serviceImpl;import com.springboottime.time.service.AdviceService;import lombok.Data;@Datapublic class AdviceServiceImpl implements AdviceService { private String name; @Override public String findUser() { System.out.println("***************执行业务方法findUser,查找的用户名字为:"+name+"****************"); return name; } @Override public void addUser() { System.out.println("***************执行业务方法addUser****************"); throw new RuntimeException(); }}
切面类:
package com.springboottime.time.aop;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;public class AopAspect { /** * 前置通知:目标方法调用之前执行的代码 * @param jp */ public void doBefore(JoinPoint jp){ System.out.println("===========执行前置通知============"); } /** * 后置返回通知:目标方法正常结束后执行的代码 * 返回通知是可以访问到目标方法的返回值的 * @param jp * @param result */ public void doAfterReturning(JoinPoint jp,String result){ System.out.println("===========执行后置通知============"); System.out.println("返回值result==================="+result); } /** * 最终通知:目标方法调用之后执行的代码(无论目标方法是否出现异常均执行) * 因为方法可能会出现异常,所以不能返回方法的返回值 * @param jp */ public void doAfter(JoinPoint jp){ System.out.println("===========执行最终通知============"); } /** * * 异常通知:目标方法抛出异常时执行的代码 * 在目标方法执行的时候,如果抛出异常,立即进入此方法 * 可以访问到异常对象 * @param jp * @param ex */ public void doAfterThrowing(JoinPoint jp,Exception ex){ System.out.println("===========执行异常通知============"); } /** * 环绕通知:目标方法调用前后执行的代码,可以在方法调用前后完成自定义的行为。 * 包围一个连接点(join point)的通知。它会在切入点方法执行前执行同时方法结束也会执行对应的部分。 * 主要是调用proceed()方法来执行切入点方法,来作为环绕通知前后方法的分水岭。 * * 环绕通知类似于动态代理的全过程:ProceedingJoinPoint类型的参数可以决定是否执行目标方法。 * 而且环绕通知必须有返回值,返回值即为目标方法的返回值 * @param pjp * @return * @throws Throwable */ public Object doAround(ProceedingJoinPoint pjp) throws Throwable{ System.out.println("======执行环绕通知开始========="); // 调用方法的参数 Object[] args = pjp.getArgs(); // 调用的方法名 String method = pjp.getSignature().getName(); // 获取目标对象 Object target = pjp.getTarget(); // 执行完方法的返回值 // 调用proceed()方法,就会触发切入点方法执行 Object result=pjp.proceed(); //如果调用pjp.proceed()执行业务方法的时候抛出异常,那么下面的代码将不会执行 System.out.println("输出,方法名:" + method + ";目标对象:" + target + ";返回值:" + result); System.out.println("======执行环绕通知结束========="); return result; }}
Spring的AOP配置: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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> <!-- 声明一个业务类 --> <bean id="userManager" class="com.springboottime.time.service.serviceImpl.AdviceServiceImpl"> <property name="name" value="lixiaoxi"></property> </bean> <!-- 声明通知类 --> <bean id="aspectBean" class="com.springboottime.time.aop.AopAspect" /> <aop:config> <aop:aspect ref="aspectBean"> <aop:pointcut id="pointcut" expression="execution(* com.springboottime.time.service.serviceImpl.AdviceServiceImpl..*(..))"/> <aop:before method="doBefore" pointcut-ref="pointcut"/> <aop:after-returning method="doAfterReturning" pointcut-ref="pointcut" returning="result"/> <aop:after method="doAfter" pointcut-ref="pointcut" /> <aop:around method="doAround" pointcut-ref="pointcut"/> <aop:after-throwing method="doAfterThrowing" pointcut-ref="pointcut" throwing="ex"/> </aop:aspect> </aop:config></beans>
springboot启动类设置:注意要引入xml文件
package com.springboottime.time;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.ImportResource;@ImportResource("classpath:spring-aop.xml")//@ImportResource("classpath:spring-aop-aspectJ.xml")@SpringBootApplicationpublic class TimeApplication { public static void main(String[] args) { SpringApplication.run(TimeApplication.class, args); }}
测试类:
package com.springboottime.time;import com.springboottime.time.service.AdviceService;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;@RunWith(SpringRunner.class)@SpringBootTestpublic class TimeApplicationTests { @Autowired private AdviceService adviceService; @Test public void contextLoads() { } @Test public void test(){ String user = adviceService.findUser(); System.out.println("<><><><><><><><><><><><><>"); adviceService.addUser(); }}
测试结果:
===========执行前置通知==================执行环绕通知开始=========***************执行业务方法findUser,查找的用户名字为:lixiaoxi****************输出,方法名:findUser;目标对象:AdviceServiceImpl(name=lixiaoxi);返回值:lixiaoxi======执行环绕通知结束====================执行最终通知=======================执行后置通知============返回值result===================lixiaoxi<><><><><><><><><><><><><>===========执行前置通知==================执行环绕通知开始=========***************执行业务方法addUser****************===========执行异常通知=======================执行最终通知============java.lang.RuntimeExceptionat com.springboottime.time.service.serviceImpl.AdviceServiceImpl.addUser(AdviceServiceImpl.java:20)
二、使用注解配置AOP
采用注解来做aop, 主要是将写在spring 配置文件中的连接点写到注解里面。
业务接口和业务实现与上边一样,具体切面类如下:
package com.springboottime.time.aop;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;@Aspectpublic class AopAspectJ { /** * 必须为final String类型的,注解里要使用的变量只能是静态常量类型的 */ public static final String EDP="execution(* com.springboottime.time.service.serviceImpl.AdviceServiceImpl..*(..))"; /** * 切面的前置方法 即方法执行前拦截到的方法 * 在目标方法执行之前的通知 * @param jp */ @Before(EDP) public void doBefore(JoinPoint jp){ System.out.println("=========AopAspectJ执行前置通知=========="); } /** * 在方法正常执行通过之后执行的通知叫做返回通知 * 可以返回到方法的返回值 在注解后加入returning * @param jp * @param result */ @AfterReturning(value=EDP,returning="result") public void doAfterReturning(JoinPoint jp,String result){ System.out.println("===========AopAspectJ执行后置通知============"); } /** * 最终通知:目标方法调用之后执行的通知(无论目标方法是否出现异常均执行) * @param jp */ @After(value=EDP) public void doAfter(JoinPoint jp){ System.out.println("===========AopAspectJ执行最终通知============"); } /** * 在目标方法非正常执行完成, 抛出异常的时候会走此方法 * @param jp * @param ex */ @AfterThrowing(value=EDP,throwing="ex") public void doAfterThrowing(JoinPoint jp,Exception ex) { System.out.println("===========AopAspectJ执行异常通知============"); } /** * 环绕通知:目标方法调用前后执行的通知,可以在方法调用前后完成自定义的行为。 * @param pjp * @return * @throws Throwable */ @Around(EDP) public Object doAround(ProceedingJoinPoint pjp) throws Throwable{ System.out.println("======AopAspectJ执行环绕通知开始========="); // 调用方法的参数 Object[] args = pjp.getArgs(); // 调用的方法名 String method = pjp.getSignature().getName(); // 获取目标对象 Object target = pjp.getTarget(); // 执行完方法的返回值 // 调用proceed()方法,就会触发切入点方法执行 Object result=pjp.proceed(); System.out.println("输出,方法名:" + method + ";目标对象:" + target + ";返回值:" + result); System.out.println("======AopAspectJ执行环绕通知结束========="); return result; }}
spring的配置:spring-aop-aspectJ.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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> <!-- 声明spring对@AspectJ的支持 --> <aop:aspectj-autoproxy/> <!-- 声明一个业务类 --> <bean id="userManager" class="com.springboottime.time.service.serviceImpl.AdviceServiceImpl"> <property name="name" value="lixiaoxi"></property> </bean> <!-- 声明通知类 --> <bean id="aspectBean" class="com.springboottime.time.aop.AopAspectJ" /></beans>
springboot启动类配置:
package com.springboottime.time;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.ImportResource;//@ImportResource("classpath:spring-aop.xml")@ImportResource("classpath:spring-aop-aspectJ.xml")@SpringBootApplicationpublic class TimeApplication { public static void main(String[] args) { SpringApplication.run(TimeApplication.class, args); }}
测试结果:
======AopAspectJ执行环绕通知开始==================AopAspectJ执行前置通知==========***************执行业务方法findUser,查找的用户名字为:lixiaoxi****************输出,方法名:findUser;目标对象:AdviceServiceImpl(name=lixiaoxi);返回值:lixiaoxi======AopAspectJ执行环绕通知结束====================AopAspectJ执行最终通知=======================AopAspectJ执行后置通知============<><><><><><><><><><><><><>======AopAspectJ执行环绕通知开始==================AopAspectJ执行前置通知==========***************执行业务方法addUser****************===========AopAspectJ执行最终通知=======================AopAspectJ执行异常通知============java.lang.RuntimeExceptionat com.springboottime.time.service.serviceImpl.AdviceServiceImpl.addUser(AdviceServiceImpl.java:20)
入门级例子。
引用一个猴子偷桃,守护者守护果园抓住猴子的小情节。
1、猴子偷桃类(普通类):
package com.samter.common; /** * 猴子 * @author Administrator * */ public class Monkey { public void stealPeaches(String name){ System.out.println("【猴子】"+name+"正在偷桃..."); } }
2、守护者类(声明为Aspect):
package com.samter.aspect; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; /** * 桃园守护者 * @author Administrator * */ @Aspect public class Guardian { /** * 定义一个方法,用于声明切入点表达式。一般的,该方法中再不需要添加其他的代码 * 使用@Pointcut 来声明切入点表达式 * 后面的其他通知直接使用方法名直接引用方法名即可 */ @Pointcut("execution(* com.samter.common.Monkey.stealPeaches(..))") public void foundMonkey(){} @Before(value="foundMonkey()") public void foundBefore(){ System.out.println("【守护者】发现猴子正在进入果园..."); } @AfterReturning("foundMonkey() && args(name,..)") public void foundAfter(String name){ System.out.println("【守护者】抓住了猴子,守护者审问出了猴子的名字叫“"+name+"”..."); } }
3、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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd" > <!-- 定义Aspect --> <bean id="guardian" class="com.samter.aspect.Guardian" /> <!-- 定义Common --> <bean id="monkey" class="com.samter.common.Monkey" /> <!-- 启动AspectJ支持 --> <aop:aspectj-autoproxy /> </beans>
4、测试类:
package com.samter.common; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("config.xml"); Monkey monkey = (Monkey) context.getBean("monkey"); try { monkey.stealPeaches("孙大圣的大徒弟"); } catch(Exception e) {} } }
5、控制台输出:
【守护者】发现猴子正在进入果园... 【猴子】孙大圣的大徒弟正在偷桃... 【守护者】抓住了猴子,守护者审问出了猴子的名字叫“孙大圣的大徒弟”...
解说:
1写了一个猴子正在偷桃的方法。
2写了一个标志为@Aspect的类,它是守护者。它会在猴子偷桃之前发现猴子,并在猴子偷桃之后抓住猴子。
原理:
A、@Aspect的声明表示这是一个切面类。
B、@Pointcut使用这个方法可以将com.samter.common.Monkey.stealPeaches(..)方法声明为poincut即切入点。作用,在stealPeaches方法被调用的时候执行2的foundMonkey方法。其中execution是匹配方法执行的切入点,也就是spring最常用的切入点定义方式。
C、@Before(value="foundMonkey()"):@Before声明为在切入点方法执行之前执行,而后面没有直接声明切入点,而是value="foundMonkey()",是因为如果@afterReturning等都有所改动的时候都必须全部改动,所以统一用Pointcut的foundMonkey代替,这样子有改动的时候仅需改动一个地方。其他@AfterReturning类同。
3是xml配置文件,里面有具体的注释。
特别说明:Guardian类里面的@Pointcut("execution(* com.samter.common.Monkey.stealPeaches(..))"),如果stealPeaches有参数则..表示所有参数,@AfterReturning("foundMonkey() && args(name,..)")的&& args(name,..)可以获取切入点方法stealPeaches的参数。
总结:这里列举了一个简单的例子,但是不难引申到应用中,当你写一个登陆系统的时候,你或许要记录谁成功登陆了系统,谁登陆系统密码错误等等的信息,这样子你用切面是再合适不过的了,总之当你的事务逻辑都设计到日志、安全检查、事务管理等等共同的内容的时候,用切面是要比你没有一个事务逻辑类都有相关代码或者相关引用好得多。