前言
上节我们着重讲解了什么是AOP,那么本节我们就要具体来讲讲AOP的实际用途,Spring的AOP开发就是一个很好的栗子,研究Spring的AOP可以帮助我们更深入的了解什么是面向切面编程
基于XML方式的普通AOP开发
接下我们来看看Spring中的AOP开发模式,在学习之前,还是先了解下Spring中AOP相关的一些概念
AOP开发中的概念:
JoinPoint(连接点):所谓连接点是指那些被拦截的点,而spring中这些点就是指方法,因为spring只支持方法类型的连接点。
PointCut(切入点):所谓切入点就是指我们要对那些JoinPoint进行拦截的定义。
Advice(通知/增强):所谓通知/增强,就是指拦截到JoinPoint后需要完成的事情。他分为前置通知/增强,后置通知/增强,异常通知/增强,最终通知/增强,环绕通知/增强(切面要完成的功能)
Introduction(引介):引介是一种特殊的Advice,在不修改代码的前提下,引介可以在运行期为类动态的添加一些方法或Field
Target(目标):代理对象的目标对象(要增强的类)
Weaving(织入):把Advice应用到Target的过程
Proxy(代理):一个类被AOP注入增强后,就产生了一个结果代理类
Aspect(切面):是PointCut和Advice(Introduction)的结合
那么在开发前,我们的准备工作一定要准备好,注意了,这次是一定要导入一个非常重要的jar包,就是
aspectjweaver
,听名字不难推断出它就是用于Weaving(织入)功能的jar
还有一个注意点就是在namespace处勾选aop,并且勾选Namespace Versions的第一个无版本号的aop.xsd
好啦,基本需要导入的包导入完成之后,我们就开始建项目了,结构如下
RailwayTicket火车站售票类(Target)
public class RailwayTicket {
public void buyTicket() {
System.out.println("buy tickets");
}
public void searchTicket(String name) {
System.out.println(name + " search tickets");
}
}
EnterAlibabaApp(Before)
public class EnterAlibabaApp implements MethodBeforeAdvice{
@Override
public void before(Method arg0, Object[] arg1, Object arg2) {
System.out.println("打开支付宝购票系统");
}
}
ExitAlibabaApp(After)
public class ExitAlibabaApp implements AfterReturningAdvice{
@Override
public void afterReturning(Object arg0, Method arg1, Object[] arg2, Object arg3) throws Throwable {
System.out.println("退出支付宝购票系统");
}
}
EntryTimeStatistics(Around)
public class EntryTimeStatistics implements MethodInterceptor{
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
Object proceed = invocation.proceed();//执行被代理对象的方法
long endTime = System.currentTimeMillis();
System.out.println("用户登录时长:" + (endTime - startTime));
return proceed;
}
}
AppException(Exception)
public class AppException implements ThrowsAdvice{
public void afterThrowing(Throwable throwable) {
System.out.println("系统出现异常:" + throwable.getMessage() + ",请重新登录");
}
}
基本的类都建好了,那我在括号里面标注的英文都是什么意思呢?
首先Target之前我们碰到过吧,Target翻译过来就是目标,在这里我们的目标就是被代理对象,也就是我们项目中的RailwayTicket类
Before就是前调通知,After就是后置通知,Around就是环绕通知,Exception就是异常通知
接下来就是我们的配置文件applicationContext.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-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="railwayTicket" class="com.marco.domain.RailwayTicket"></bean>
<bean id="enterAlibabaApp" class="com.marco.advise.EnterAlibabaApp"></bean>
<bean id="exitAlibabaApp" class="com.marco.advise.ExitAlibabaApp"></bean>
<bean id="entryTimeStatistics" class="com.marco.advise.EntryTimeStatistics"></bean>
<bean id="appException" class="com.marco.advise.AppException"></bean>
<aop:config>
<aop:pointcut expression="execution(* com.marco.domain.*.*(..))" id="point"/>
<aop:advisor advice-ref="enterAlibabaApp" pointcut-ref="point"/>
<aop:advisor advice-ref="exitAlibabaApp" pointcut-ref="point"/>
<aop:advisor advice-ref="entryTimeStatistics" pointcut-ref="point"/>
<aop:advisor advice-ref="appException" pointcut-ref="point" />
</aop:config>
</beans>
aop:config
标签顾名思义包裹的就是aop配置信息,aop:pointcut
指的就是我们的切入点
expression="execution(* com.marco.domain.*.*(..))
是一个表达式,它表达的意思就是在com.marco.domain包下的所有类的所有方法和所有参数都被纳入切入点的范围之内
advice-ref
和pointcut-ref
不说大家也应该能够猜到它们分别是通知对象的引用和切入点对象的引用了
所有的配置和类都建立完成了,接下来我们就来测试一下吧~
public class Client {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
RailwayTicket railwayTicket = context.getBean(RailwayTicket.class);
railwayTicket.searchTicket("marco");
}
}
我们发现前置通知、后置通知、环绕通知的效果都出来,在这里大家有没有疑问,就是我们有前置和后置,为什么还要加一个环绕通知呢?
大家看我这个案例应该可以发现我是用环绕通知记录了客户登录系统所使用的时间对吧?
设想如果使用前置通知和后置通知我们如何能记录下这个时间呢?或许我们可以把startTime声明为成员变量,但是如果涉及到的线程比较多,那么我们记录的时间很有可能不准确,这就涉及到多线程的问题了。
好,那前三个通知都演示出来效果,我们再来演示一下异常通知的效果,在此之前我们改一下RailwayTicket的代码
public class RailwayTicket {
public void buyTicket() {
System.out.println("buy tickets");
}
public void searchTicket(String name) {
int i = 10/0;
System.out.println(name + " search tickets");
}
}
可以看到当抛出异常时,异常通知就会被正常执行,需要引起注意的一点是,Spring不知是处于什么考量,并没有给ThrowsAdvice这个接口设定任何方法,但是我们主要手动写出来,并且名字一定要是afterThrowing
,否则程序也不会被正常执行!
public void afterThrowing(Throwable throwable) {
System.out.println("系统出现异常:" + throwable.getMessage() + ",请重新登录");
}
基于XML方式的AspectJ AOP开发
又来了一个陌生的名词,老规矩,还是给大家先介绍一下什么是AspectJ
AspectJ: 一个基于Java语言的AOP框架,Spring2.0开始引入对AspectJ的支持,AspectJ扩展了Java语言,提供了专门的编译器,在编译时提供了横向代码的注入。
@AspectJ是AspectJ1.5新增功能,通过JDK1.5注解技术,允许直接在Bean中定义切面
新版本的Spring中,建议使用AspectJ方式开发AOP
没错,我们的老朋友"注解"又回来了!不过AspectJ开发是有两种模式的,虽然我知道大家都很喜欢注解,毕竟简单好用,但是原始的方式还是得了解下啦,还是先看看这次重构后得目录结构吧!
domain得内容不变,将之前得adviser包里所有得类集合成一个类
AppAdvisor
public class AppAdvisor {
public void before() {
System.out.println("打开支付宝购票系统");
}
public void after() {
System.out.println("退出支付宝购票系统");
}
public void around(ProceedingJoinPoint point) {
long startTime = System.currentTimeMillis();
try {
point.proceed();
} catch (Throwable e) {
e.printStackTrace();
}//执行被代理对象的方法
long endTime = System.currentTimeMillis();
System.out.println("用户登录时长:" + (endTime - startTime));
}
public void error(Throwable throwable) {
System.out.println("系统出现异常:" + throwable.getMessage() + ",请重新登录");
}
那使用这种方式,配置文件该这么写
<?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-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="railwayTicket" class="com.marco.domain.RailwayTicket"></bean>
<bean id="appAdvisor" class="com.marco.advise.AppAdvisor"></bean>
<aop:config>
<aop:aspect ref="appAdvisor">
<aop:pointcut expression="execution(* com.marco.domain.*.*(..))" id="point"/>
<aop:before method="before" pointcut-ref="point"/>
<aop:after method="after" pointcut-ref="point"/>
<aop:around method="around" pointcut-ref="point"/>
<aop:after-throwing method="error" pointcut-ref="point" throwing="throwable"/>
</aop:aspect>
</aop:config>
</beans>
其实之前在写第一种aop得时候,我们应该发现了在<aop:config>
中有<aop:aspect>
这么个标签,这个标签集成了所有的aop属性,通过它里面的标签,我们很容易就能够区分每一个标签的功能是什么,大家有没有发现,循序渐进我们的配置越来越简单了,相信上面这个配置也难不倒在座各位~
注意点:
<aop:after-throwing>
中需要加上throwing="throwable"
意思就是告诉Spring我的异常参数是什么,便于Spring在解析的时候能够找的到<aop:aspect>
中需要加上ref="appAdvisor"
,告诉Spring,我的全部通知都放在id为appAdvisor的bean里面了,你自己去找吧!
基于注解方式的AspectJ AOP开发
没错!还没完!既然上面说到循序渐进,我们的配置会越来越简单,这次结合注解,让你体会到飞一般的感觉!
之前的代码还是不变,修改一下AppAdvisor 类
@Component//让Srping的IoC容器创建对象并将对象放入容器
@Aspect//告诉Spring,这个类是一个切面类
@EnableAspectJAutoProxy
public class AppAdvisor {
@Pointcut(value="execution(* com.marco.domain.*.*(..))")
public void point() {
}
//@Before(value="execution(* com.marco.domain.*.*(..))")
@Before(value="point()")
public void before() {
System.out.println("打开支付宝购票系统");
}
//@After(value="execution(* com.marco.domain.*.*(..))")
@After(value="point()")
public void after() {
System.out.println("退出支付宝购票系统");
}
//@Around(value="execution(* com.marco.domain.*.*(..))")
@Around(value="point()")
public void around(ProceedingJoinPoint point) {
long startTime = System.currentTimeMillis();
try {
point.proceed();
} catch (Throwable e) {
e.printStackTrace();
}//执行被代理对象的方法
long endTime = System.currentTimeMillis();
System.out.println("用户登录时长:" + (endTime - startTime));
}
//@AfterThrowing(value="execution(* com.marco.domain.*.*(..))",throwing="throwable")
@AfterThrowing(value="point()",throwing="throwable")
public void error(Throwable throwable) {
System.out.println("系统出现异常:" + throwable.getMessage() + ",请重新登录");
}
}
接下来我们就只用配置扫描就可以啦
<?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-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.marco.domain"></context:component-scan>
<context:component-scan base-package="com.marco.advise"></context:component-scan>
<!-- 开启aop的自动代理,spring框架会解析切面注解,使用@EnableAspectJAutoProxy后可以省略!-->
<!-- <aop:aspectj-autoproxy></aop:aspectj-autoproxy> -->
</beans>
测试结果也一样,是不是感觉使用注解之后真的很爽!