一、AOP的引入
这里可以把单个模块当做是一圆柱,假如没有aop,在做日志处理的时候,我们会在每个模块中添加日志或者权限处理,日志或权限类似圆柱体的部分圆柱。
一般大多数的日志或权限处理代码是相同的,为了实现代码复用,我们可能把日志处理抽离成一个新的方法。
但是这样我们仍然必须手动插入这些方法,而且这两个方法就是强耦合的,假如此时我们不需要这个功能了,或者想换成其他功能,那么就必须一个个修改。
通过动态代理,可以在指定位置执行对应流程。这样就可以将一些横向的功能抽离出来形成一个独立的模块,然后在指定位置插入这些功能。这样的思想,被称为面向切面编程,亦即AOP。
二、AOP主要概念
上小节介绍了引入AOP的好处,本小节来了解下AOP的几个核心概念。
1.横切关注点
AOP把一个业务流程分成几部分,例如权限检查、业务处理、日志记录,每个部分单独处理,然后把它们组装成完整的业务流,每部分被称为切面或关注点。
2.切面
类是对物体特征的抽象,切面就是对横切关注点的抽象。可以每部分抽象成一叠纸一样一层一层的,那每张纸都是一切面。
3.连接点
被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器.其实Spring只支持方法类型的连接点就包含字段和构造器。为啥呢?因为字段它是通过get,set方法,构造器它其实也是方法。Spring只支持方法类型的连接点和连接点是字段或者构造器它们是包含关系。
4.切入点
对连接点进行拦截的定义,连接点可以很多,但并不一定每个连接点都进行操作,比如莲藕,藕段与藕段之间它们是有连接点的,但不一定都切开。
5.通知
通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类,这个有点类似把藕段与藕段断开之后要做的事情,是往里面加蜂蜜还是做什么。
6.目标对象
代理的目标对象,就是动态代理的target,在实际操作中一般会先实现AOP的接口,然后配置这些接口作用到哪些对象上,被作用的对象就是目标对象。
7.织入
切面是独立的,目标对象也是独立的,它们是不耦合的,那它怎么把切面放到目标对象中呢,这时就需要进行织入操作,就类似一中的,怎么把target和打印日志联系到一起呢,那就需要动态代理,在spring中aop.framework.ProxyFactory就是用作织入器,来进行横切逻辑的织入。
8.引入
不改代码的同时,为类动态的添加方法或字段。
三、AOP实现
前面小节主要介绍了AOP的理论知识,本小节通过示例进一步理解Spring中AOP的使用。主要介绍AOP的三种实现方式:经典的基于代理的AOP、AspectJ基于XML的配置、AspectJ基于注解的配置。
一、经典的基于代理的AOP
基于代理的AOP主要介绍MethodBeforeAdvice、AfterReturningAdvice、ThrowsAdvice三个接口的使用。
MethodBeforeAdvice:见名知意,通过方法名就可以猜到是它的作用。方法前拦截器在执行指定方法前调用,参数分别为被调用的方法、执行时被传入的参数、被拦截的bean。
AfterReturningAdvice:返回后拦截器在执行完指定方法并返回之后调用。如果有返回值可以获取到返回值,否则为null。参数分别为方法返回值、被调用的方法、执行时被传入的参数、被拦截的bean。
ThrowsAdvice:异常拦截器在指定方法抛出异常时被调用。该接口并未定义方法,因此不需要实现任何方法。那它是怎么拦截的呢?我们可以查看该接口的定义,在定义类文档中有如下图的说明。如果在实现该接口的类中定义了如public void afterThrowing(Exception ex)、public void afterThrowing(Method method, Object[] args, Object target, Exception ex)方法抛出异常时就会被调用。
在软件开发中推荐面向接口的编程,所以这里创建了一个IAOPServices接口,并定义了两个方法。withAopMethod方法将使用拦截器拦截的方法,withNoAopMethod方法不会被拦截器拦截。接口代码如下:
package basicAop; public interface IAOPServices { public String withAopMethod() throws Exception; public String withNoAopMethod() throws Exception; }
在AOPServicesImpl类中实现了该接口,并在该类中定义了String类型的description属性,以及对应的getter、setter方法。两个接口方法将返回该description属性的值。
package basicAop; public class AOPServicesImpl implements IAOPServices { private String description; public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String withAopMethod() throws Exception { System.out.println("AOP函数运行方法:withAopMethod"); if(description.trim().length()==0){ throw new Exception("description属性不能为空"); } return description; } public String withNoAopMethod() throws Exception { System.out.println("无AOP函数运行方法:withNoAopMethod"); return description; } }
上面把要使用AOP拦截的方法准备好了,下面就是定义AOP拦截器方法了。这里在AOPInterceptor类中实现了上面的AfterReturningAdvice,MethodBeforeAdvice,ThrowsAdvice三个接口。
package basicAop; import java.lang.reflect.Method; import org.springframework.aop.AfterReturningAdvice; import org.springframework.aop.MethodBeforeAdvice; import org.springframework.aop.ThrowsAdvice; public class AOPInterceptor implements AfterReturningAdvice,MethodBeforeAdvice,ThrowsAdvice { public void afterReturning(Object value, Method method, Object[] args, Object instance) throws Throwable { System.out.println("方法"+method.getName()+"运行结束,返回值为:"+value); } public void before(Method method, Object[] args, Object instance) throws Throwable { System.out.println("执行MethodBeforeAdvice,即将执行的方法:"+method.getName()); if(instance instanceof AOPServicesImpl) { String description=((AOPServicesImpl)instance).getDescription(); if(description==null) { throw new NullPointerException("description属性不能为null"); } } } public void afterThrowing(Exception ex){ System.out.println("抛出了异常:"+ex.getMessage()); } public void afterThrowing(Method method, Object[] args, Object target, Exception ex){ System.out.println("方法"+method.getName()+"抛出了异常:"+ex.getMessage()); } }
这里要拦截的方法和拦截器都准备好了,那怎么将该拦截器用于拦截该方法呢?这里就需要进行配置。首先在pom.xml中引入依赖,这里引入spring-aop、spring-context。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.demo</groupId> <artifactId>BasicAOP</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>5.0.0.RELEASE</spring.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> </dependencies> </project>
实际上Spring无法将Services实现类与拦截器直接组装,因为没有对应的setter、getter方法。安装时借助 Spring中的代理类,将自定义拦截器注入到NameMatchMethodPointcutAdvisor类中的advice属性中,再将定义好的NameMatchMethodPointcutAdvisor对象注入到ProxyFactoryBean。这里将自定义的AOPInterceptor拦截器注入到NameMatchMethodPointcutAdvisor中,然后将NameMatchMethodPointcutAdvisor对象注入到ProxyFactoryBean中。
<?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"> <bean id="aopInterceptor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor"> <property name="advice"> <bean class="basicAop.AOPInterceptor"></bean> </property> <property name="mappedName" value="withAopMethod"></property> </bean> <bean id="aopService" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="interceptorNames"> <list> <value>aopInterceptor</value> </list> </property> <property name="target"> <bean class="basicAop.AOPServicesImpl"> <property name="description" value="basicAop"></property> </bean> </property> </bean> </beans>
从Spring容器中获取IAOPServices对象,并分别执行IAOPServices中的两个方法。Spring会在withAopMethod()方法前后添加拦截器,在withNoAopMethod()方法前后并不会添加。
package basicAop; import org.springframework.beans.factory.BeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class AopTest { public static void main(String[] args) throws Exception { ApplicationContext context=new ClassPathXmlApplicationContext(new String[]{"applicationContext.xml"}); BeanFactory factory=context; IAOPServices services=(IAOPServices)context.getBean("aopService"); services.withAopMethod(); services.withNoAopMethod(); } }
运行结果如下:
二、AspectJ基于XML的配置
AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。我们还是利用IAOPServices接口、AOPServicesImpl类实现AspectJ基于XML的AOP编程。下表是AspectJ主要的配置元素。使用AspectJ时需要引入两个依赖项aopalliance、aspectjweaver。在引入这两个依赖项时需要注意,有时报错误,更新下两个依赖项的版本就好了。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.demo</groupId> <artifactId>AutoScanAOP</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>5.0.0.RELEASE</spring.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>aopalliance</groupId> <artifactId>aopalliance</artifactId> <version>1.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.11</version> </dependency> </dependencies> </project>
AOP配置元素 |
描述 |
<aop:config> |
顶层的AOP配置元素,大多数的<aop:*>元素必须包含在<aop:config>元素内 |
<aop:aspect> |
定义切面 |
<aop:aspect-autoproxy> |
启用@AspectJ注解驱动的切面 |
<aop:pointcut> |
定义切点 |
<aop:advisor> |
定义AOP通知器 |
<aop:before> |
定义AOP前置通知 |
<aop:after> |
定义AOP后置通知(不管被通知的方法是否执行成功) |
<aop:after-returning> |
定义成功返回后的通知 |
<aop:after-throwing> |
定义抛出异常后的通知 |
<aop:around> |
定义AOP环绕通知 |
<aop:declare-parents> |
为被通知的对象引入额外的接口,并透明地实现 |
这里定义了XMLAdvice拦截器方法,用于演示前置、后置、成功返回、异常返回、环绕通知。
package Services; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; public class XMLAdvice { public void beforeAdvice() { System.out.println("前置通知执行了"); } public void afterAdvice() { System.out.println("后置通知执行了"); } public void afterReturnAdvice(String result) { System.out.println("返回通知执行了" + "运行业务方法返回的结果为" + result); } public String aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { String result = ""; try { System.out.println("环绕通知开始执行了"); long start = System.currentTimeMillis(); result = (String) proceedingJoinPoint.proceed(); long end = System.currentTimeMillis(); System.out.println("环绕通知执行结束了"); System.out.println("执行业务方法共计:" + (end - start) + "毫秒。"); } catch (Throwable e) { } return result; } public void throwingAdvice(JoinPoint joinPoint, Exception e) { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append("异常通知执行了."); stringBuffer.append("方法:").append(joinPoint.getSignature().getName()).append("出现了异常."); stringBuffer.append("异常信息为:").append(e.getMessage()); System.out.println(stringBuffer.toString()); } }
上面把拦截器定义完成,之后就是把定义好的拦截器与Services关联在一起。使用AOP配置元素将Services与拦截器中的方法关联上。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" 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.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd "> <bean id="serviceImplA" class="Services.AOPServicesImpl"> <property name="description" value="basicAop"></property> </bean> <bean id="serviceAspectBean" class="Services.XMLAdvice" /> <aop:config> <aop:aspect id="serviceAspect" ref="serviceAspectBean"> <aop:pointcut id="servicePointcut" expression="execution(* Services.*.withAop*(..))" /> <aop:before pointcut-ref="servicePointcut" method="beforeAdvice" /> <aop:after pointcut-ref="servicePointcut" method="afterAdvice" /> <aop:after-returning pointcut-ref="servicePointcut" method="afterReturnAdvice" returning="result" /> <aop:around pointcut-ref="servicePointcut" method="aroundAdvice" /> <aop:after-throwing pointcut-ref="servicePointcut" method="throwingAdvice" throwing="e" /> </aop:aspect> </aop:config> </beans>
配置完之后还是和经典的基于代理的AOP一样,运行代码从Spring容器中获取IAOPServices对象,并分别执行IAOPServices中的两个方法。Spring会在withAopMethod()方法前后添加拦截器,在withNoAopMethod()方法前后并不会添加。
package AspectJAOP; import org.springframework.beans.factory.BeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import Services.IAOPServices; public class App { public static void main(String[] args) throws Exception { ApplicationContext context=new ClassPathXmlApplicationContext(new String[]{"applicationContext.xml"}); BeanFactory factory=context; IAOPServices services=(IAOPServices)context.getBean("serviceImplA"); services.withAopMethod(); services.withNoAopMethod(); } }
运行结果如下:
三、AspectJ基于注解的配置
AspectJ基于XML的配置还是需要在XML中配置AOP元素,现在一般提倡使用注解来进行编程,AspectJ也提供了基于注解的实现方式。基于注解的AOP配置其实和基于XML的一样,可以参照基于XML的来进行理解。这里定义了AnnontationAdvice,并用@Aspect注解定义切面。在XML中的配置元素改成了注解关键字。
package Services; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Component @Aspect public class AnnontationAdvice { @Before("execution(* Services.*.withAop*(..))") public void beforeAdvice() { System.out.println("前置通知执行了"); } @After("execution(* Services.*.withAop*(..))") public void afterAdvice() { System.out.println("后置通知执行了"); } @AfterReturning(value="execution(* Services.*.withAop*(..))",returning="result") public void afterReturnAdvice(String result) { System.out.println("返回通知执行了" + "运行业务方法返回的结果为" + result); } @Around(value="execution(* Services.*.withAop*(..))") public String aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { String result = ""; try { System.out.println("环绕通知开始执行了"); long start = System.currentTimeMillis(); result = (String) proceedingJoinPoint.proceed(); long end = System.currentTimeMillis(); System.out.println("环绕通知执行结束了"); System.out.println("执行业务方法共计:" + (end - start) + "毫秒。"); } catch (Throwable e) { } return result; } @AfterThrowing(value="execution(* Services.*.withAop*(..))",throwing="e") public void throwingAdvice(JoinPoint joinPoint, Exception e) { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append("异常通知执行了."); stringBuffer.append("方法:").append(joinPoint.getSignature().getName()).append("出现了异常."); stringBuffer.append("异常信息为:").append(e.getMessage()); System.out.println(stringBuffer.toString()); } }
在配置文件中只需配置下自动扫描的包名,并配置下<aop:aspectj-autoproxy>即可,比XML的配置简单一些。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" 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.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd "> <!-- 配置自动扫描的包 --> <context:component-scan base-package="Services"></context:component-scan> <!-- 自动为切面方法中匹配的方法所在的类生成代理对象。 --> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans> <bean id="serviceImplA" class="Services.AOPServicesImpl"> <property name="description" value="basicAop"></property> </bean>
最后还是从Spring容器中获取IAOPServices对象,并分别执行IAOPServices中的两个方法。运行结果如下图,从打印的日志可以看到拦截器拦截了withAopMethod()方法,withNoAopMethod()并未拦截。
运行结果如下: