这篇博客较长,耐心读完或许会有“柳暗花明又一村”的感觉哦!
为什么?
我先不说AOP是什么,我先说说为什么要用AOP,依照惯例,我还是先举一个例子:
-
先把项目结构展现出来:
-
我们先在com.jd.calculator.service里定义一个接口
ICalculatorService
:package com.jd.calculator.service; public interface ICalculatorService { //定义几个简单的加减法 //加法 int add(int a, int b); //除法 int div(int a, int b); //乘法 int mul(int a, int b); }
-
然后去实现这个接口:
package com.jd.calculator.service; import org.springframework.stereotype.Service; @Service public class CalculatorService implements ICalculatorService{ @Override public int add(int a, int b) { System.out.println("The add method begins"); System.out.println("The parameters of the add method are"+a+","+b); int result = a + b; return result; } @Override public int div(int a, int b) { System.out.println("The div method begins"); System.out.println("The parameters of the div method are"+a+","+b); int result = a / b; return result; } @Override public int mul(int a, int b) { System.out.println("The mul method begins"); System.out.println("The parameters of the mul method are"+a+","+b); int result = a * b; return result; } }
我们会发现上面的三个方法除了方法名和具体的计算不同,其他的几乎完全一样,这就造成代码的冗余,开发的效率也会很低。
虽然我们可以再定义一个方法,把相同的部分放进去,然后其他方法再调用它。但假如我们的需求变了,不单单是在输出结果之前要输出一些东西,我们还想要输出结果之后再输出一些东西,出错了也要输出一些东西,那这时如果还采取这种方法就也会显得很复杂了。那这时该怎么办呢?
这时就要用到AOP了。
是什么?
AOP(Aspect Oriented Programming 面向切面编程)是一种指在程序运行期间动态的将某段代码切入到指定方法的指定位置进行运行的编程方式,这种编程方式实现了代码复用,是对传统OOP(Object Oriented Programming,面向对象编程 )的补充。目前,Aspectj是Java社区里最完整最流行的AOP框架,在Spring 2.0以上版本中可以通过Aspectj注解或基于XML配置AOP。
AOP用到上面的例子就是只关注代码重复的部分。
怎么做?
第一种方法:通过注解来实现AOP
@Before
:
- 首先需要配置一些jar包:
- 在Spring 配置文件(也就是
application.xml
)里添加:
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
,它可以自动生成代理类(不理解没关系,接着往下看) - 在
com.jd.aspects
包里自定义一个切面类:CalculatorAspects
package com.jd.aspects;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect //要想生成代理类,要加这个注解
@Component
public class CalculatorAspects {
@Before("execution(public int com.jd.calculator.service.CalculatorService.*(..))")//匹配这些的才能生成代理类
public void before(JoinPoint js){
Object [] args = js.getArgs();//获取参数
String name = js.getSignature().getName();//获取方法名
System.out.println("The "+name+" method begins");
System.out.println("The parameters of the "+name+" method are "+args[0]+","+args[1]);
}
}
我们一点一点说上面的代码:
-
根据
@Component
可以在IOC容器中自动实例化(即生成对象,这里就不做过多讨论了); -
aspects是切面的意思,你可以理解成从某一点(或一系列点)切入,然后再切出。这个
CalculatorAspects
类 加上@Aspect
这个注解,就表示一个切面(即代理类),至于从哪里切入,且看第3点; -
@Before("execution(public int com.jd.calculator.service.CalculatorService.*(..))")
,我们先看Before,before什么意思?之前的意思,就是说要做一件事之前,先做另一件事情,我们看到Object [] args = js.getArgs();//获取参数 String name = js.getSignature().getName();//获取方法名 System.out.println("The "+name+" method begins"); System.out.println("The parameters of the "+name+" method are "+args[0]+","+args[1]);
这段代码和
CalculatorService
类中的System.out.println("The add method begins"); System.out.println("The parameters of the add method are"+a+","+b);
这段代码一样,具有同样的作用,而且都要在计算输出结果之前输出。这时你能够理解
@Before
这个注解的作用了吧:就是在做某件事之前,先做另一件事。那这时你又疑惑了,是哪件事之前呢?换句话说,切入点在哪呢?切入点就在public int com.jd.calculator.service.CalculatorService.*(..))
,(*
代表所有;..
代表参数,可以没有,也可以有多个),意思是说在执行com.jd.calculator.service
包中的CalculatorService
类 里的public int * ()
这种方法之前,要先执行加了@Before
注解的方法。 -
加了这个
@Aspect
注解之后就会生成一个目标类(即@Before
所指向的类)的代理类,代理什么?代理的就是重复代码的那一部分。
(以下这段话不影响其他点的理解,可以不用看)那么还有一个问题,即是谁生成的这个代理类?直接告诉你答案,当在<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
中加proxy-target-class="true"
时,是由CGLIB
生成的代理类,这个代理类是目标类的子类;若不加proxy-target-class=""
,(默认值为false),则是由JDK生成的代理类,这个代理类是接口实现类。 -
JoinPoint
就是连接点,它表示在程序中明确指明的点,就是说切点附近的东西,它里面常有的方法有:方法名 作用 例子 getSignature() getName() 获取目标方法名 add getDeclaringType().getSimpleName() 获取目标类的简单类名 CalculatorService getDeclaringTypeName() 获取目标类的完整类名 com.jd.calculator.service.CalculatorService Modifier.toString(JoinPoint.getSignature().getModifiers()) 获取目标方法声明的类型 public Object[] getArgs() 获取传入目标方法的参数[列表](从0开始) a,b getThis() 代理对象自己 com.jd.calculator.service.CalculatorService@28d18df5 getTarget() 被代理的对象 com.jd.calculator.service.CalculatorService@28d18df5
@After
、@AfterReturning
、@AfterThrowing
、@Around
:
好了,通过注解的方式实现AOP的方法已经说的大概了,到这里你是否会猜想:既然有了@Before
,那会不会有@After
,还真让你给说对了,而且不但有@After
,还有@AfterReturning
、@AfterThrowing
、@Around
。
这时聪明的你又想:既然都这样了,那它们会不会和@Before
的用法差不多啊!没错,果然smart,@After
、@AfterReturning
、@AfterThrowing
的用法确实和@Before
的用法差不多,只是@AfterReturning
和@AfterThrowing
还多了一些东西,且看下面慢慢道来:
-
@After
的用法和@Before
的用法完全一样,不一样的地方就是@After
修饰的方法是在目标方法执行后才执行的。 -
@AfterReturning
,从名字中我们可以猜到被@AfterReturning
修饰的方法是在目标方法执行后才执行的,而且还会有一个返回通知:通常是计算的结果。实现的代码例如:@AfterReturning(value = "execution(public int com.jd.calculator.service.CalculatorService.*(..))",returning = "obj")//obj就是返回通知的内容 public void afterReturning(JoinPoint js,Object obj){ String name = js.getSignature().getName();//获取方法名 System.out.println("The "+name+" method result:"+obj); }
-
@AfterThrowing
,同样的,我们可以看出被@AfterThrowing
修饰的方法是在目标方法执行后才执行的,那我就想问Throwing什么意思?异常的意思。意思就是说被@AfterThrowing
修饰的方法是在目标方法执行过程中出错,才执行的;如果目标方法没有出错,就不执行。实现的代码例如:@AfterThrowing(value = "execution(public int com.jd.calculator.service.CalculatorService.*(..))",throwing = "e") public void afterThrowing(JoinPoint js,Throwable e){ System.out.println("错误的信息:"+e.getMessage()); e.printStackTrace();//将错误打印出来 }
你发现前几个注解都是见名知意,那@Around
是干什么用的?我们先来思考一番:如果要在执行一个目标方法时,我们想分别在目标方法执行前、执行后、执行过程出错时都先输出一些东西、且返回返回一些值,那这时我们要写四个方法,还要分别标注@Before
、@After
、@AfterReturning
和@AfterThrowing
四个注解,这样很麻烦,这时就可以用@Around
了,@Around
就是用来整合前四种的,它的结构如下:
try {
try {
doBefore();// @Before注解所修饰的方法
method.invoke();// 执行目标对象内的方法(即目标方法)
} finally {
doAfter();// @After注解所修饰的方法
}
doAfterReturning();// @AfterReturning注解所修饰的方法
} catch (Throwable e) {
doAfterThrowing();// @AfterThrowing注解所修饰的方法
}
上面的代码看懂了吧?好,看懂了我们来那上面的例子实战一番:
@Around("execution(public int com.jd.calculator.service.CalculatorService.*(..))")
public Object around(ProceedingJoinPoint js){
try {
Object [] args = js.getArgs();//获取参数
String name = js.getSignature().getName();//获取方法名
Object result = null;
try {
//Before
System.out.println("The "+name+" method begins");
System.out.println("The parameters of the "+name+" method are "+args[0]+","+args[1]);
//目标方法
result = js.proceed();
}finally {
//After
System.out.println("The "+name+" method ends");
}
//AfterReturning
System.out.println("The "+name+" method result:"+result);
//返回结果
return result;
}catch (Throwable e){
//AfterThrowing
System.out.println("错误的信息:"+e.getMessage());
}
return -1;
}
在这里有几点需要特别注意一下:
- 首先就是参数类型,
@Around
修饰的方法必须声明ProceedingJoinPoint
类型的参数,该变量可以决定是否执行目标方法 ,这与上面四种生命的JoinPoint
类型不同; @Around
修饰的方法必须有返回值,且返回值类型为目标方法的返回值类型或其父类。而@Before
、@After
、@AfterRunning
和@AfterThrowing
修饰的方法是没有返回值的。
@Pointcut
还有一点就是我们在实现AOP时,都是这样写注解的:
@Before("execution(public int com.jd.calculator.service.CalculatorService.*(..))")
@After("execution(public int com.jd.calculator.service.CalculatorService.*(..))")
@AfterReturning(value = "execution(public int com.jd.calculator.service.CalculatorService.*(..))",returning = "obj")
@AfterThrowing(value = "execution(public int com.jd.calculator.service.CalculatorService.*(..))",throwing = "e")
@Around("execution(public int com.jd.calculator.service.CalculatorService.*(..))")
你发现了什么?哈哈,是不是还是代码冗余啊,所以这时可以用到另一个注解:@Pointcut
,pointcut表示一组joinPoint(连接点),以上重复的代码可以通过单独自定义一个@Pointcut
注解修饰的空方法来解决:
@Pointcut("execution(public int com.jd.calculator.service.CalculatorService.*(..))")
public void pointcut(){
}
然后其他注解调用即可:
@Before("pointcut()")
@After("pointcut()")
@AfterReturning("pointcut()")
@AfterThrowing("pointcut()")
@Around("pointcut()")
是不是简便多了?
@order(num)
如果一个目标方法匹配多个切面中相同类型增强方法(即上面四种注解修饰的方法),那这几个切面谁先执行、谁后执行呢?默认的都是按类名的字典顺序(A~Z)依次执行的;但如果加上 @order(num)
就会按照指定的顺序依次执行所有匹配的切面,其中num
数值越小,优先级越高(即被 @order(1)
修饰的切面 比 被 @order(2)
修饰的切面 先执行)。
好了,终于说完了用注解的方式去实现AOP,如果大家对AOP有了较好的理解,那接下来的用xml
文件来配置AOP就更容易理解了。
第二种方法:通过xml
文件来实现AOP:
这次我们通过xml
文件来实现AOP,也就是说像什么@Aspects
、@Order
、@Before
、@After
、@Order
等的注解都不需要再加了,
-
只需要自定义普通的类(但它通过配置后还起到切面的作用,所以我们就还叫它切面类)就可以了,例如:
package com.jd.aspects; import org.aspectj.lang.JoinPoint; public class CalculatorAspects { //定义普通的方法,然后在xml文件里进行配置 public void before(JoinPoint js){ Object [] args = js.getArgs();//获取参数 String name = js.getSignature().getName();//获取方法名 System.out.println("The "+name+" method begins"); System.out.println("The parameters of the "+name+" method are "+args[0]+","+args[1]); } public void after(JoinPoint js){ String name = js.getSignature().getName();//获取方法名 System.out.println("The "+name+" method ends"); } public void afterReturning(JoinPoint js,Object obj){ String name = js.getSignature().getName();//获取方法名 System.out.println("The "+name+" method result:"+obj); } public void afterThrowing(JoinPoint js,Throwable e){ System.out.println("错误的信息:"+e.getMessage()); } }
-
在Spring配置文件(也就是
application.xml
)里进行如下配置:
<!--因为上面的CalculatorAspects类未加'@Component'注解,所以这里需要先通过<bean></bean>来实例化对象-->
<bean id="calculatorAspects" class="com.jd.aspects.CalculatorAspects"></bean>
<!--直接在xml里配置代理类-->
<aop:config proxy-target-class="true">
<aop:pointcut id="p" expression="execution(public int com.jd.calculator.service.CalculatorService.*(..))"/>
<aop:aspect ref="calculatorAspects" order="1">
<aop:before pointcut-ref="p" method="before"></aop:before>
<aop:after pointcut-ref="p" method="after"></aop:after>
<aop:after-returning pointcut-ref="p" method="afterReturning" returning="obj"></aop:after-returning>
<aop:after-throwing pointcut-ref="p" method="afterThrowing" throwing="e"></aop:after-throwing>
</aop:aspect>
</aop:config>
我们会发现这个配置和使用注解有很多相似的地方,但更加简便:
-
其中的
proxy-target-class=""
和上面讲的一样,这里就不再赘述了; -
接着是
pointcut
和上面讲的也基本一样,这里指定一个id便于下面调用,expression=""
指向目标类; -
再往下:
<aop:aspect ref="calculatorAspects" order="1"> </aop:aspect>
这个就是具体的某个切面类了,其中
ref=""
就是指向某个切面类,order=""
就是这个切面类的优先执行顺序; -
具体的:
<aop:before pointcut-ref="p" method="before"></aop:before> <aop:after pointcut-ref="p" method="after"></aop:after> <aop:after-returning pointcut-ref="p" method="afterReturning" returning="obj"></aop:after-returning> <aop:after-throwing pointcut-ref="p" method="afterThrowing" throwing="e"></aop:after-throwing>
pointcut-ref=""
调用上面的pointcut
,指向目标类,method=""
执行切面类中具体的方法,其中after-returning
和after-throwing
分别多了一个returning=""
和throwing=""
。 -
若直接使用around(环绕增强),则可以在
CalculatorAspects
类(代理类)中只定义一个aroud()
方法即可:package com.jd.aspects; import org.aspectj.lang.JoinPoint; public class CalculatorAspects { //定义普通的方法,然后在xml文件里进行配置 public Object around(ProceedingJoinPoint js){ try { Object [] args = js.getArgs();//获取参数 String name = js.getSignature().getName();//获取方法名 Object result = null; try { //before System.out.println("The "+name+" method begins"); System.out.println("The parameters of the "+name+" method are "+args[0]+","+args[1]); //目标对象中的方法 result = js.proceed(); }finally { //after System.out.println("The "+name+" method ends"); } //afterReturning System.out.println("The "+name+" method result:"+result); return result; }catch (Throwable e){ //AfterThrowing System.out.println("+++++++++++++++"+e.getMessage()); } return -1; } }
需要注意的点也与上面的一样!
在:
<aop:aspect ref="calculatorAspects" order="1"> </aop:aspect>
里只需要写入以下配置即可:
<aop:around pointcut-ref="p" method="around"></aop:around>
好了(长舒一口气…),终于把AOP相关的主要内容给说完了,不知你从我这里收获到一些没有?