七、Spring AOP
AOP 的全称是“Aspect Oriented Programming”,即面向切面编程,能隔离业务逻辑的各个部分,减少重复代码,达到解耦合,专注于核心业务,从而提高了开发效率
切面:新增功能,非业务功能,可独立使用
在写功能前我们要先确定“www”,好目标切面(who),而这个切面在什么时候执行(when),最后要确定好切面执行的位置(where)
AOP 实现原理就是动态代理,动态代理的实现方式有很多种,而AOP是动态代理的一种规范,让开发人员统一使用
目前最流行的 AOP 框架
- Spring AOP(少用):使用纯 Java 实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码
- AspectJ(开源):基于 Java 语言的 AOP 框架,AspectJ 扩展了 Java 语言,提供了强大的切面实现,在spring中有其集成
AOP专业术语:
-
Joinpoint(连接点):链接业务方法和切面的位置,就是某类种的业务方法
指向业务方法(即目标方法),作用是可以在通知方法中获取执行时的信息,如方法名、方法实参;如果我们的切面功能需要用到这些信息,就加入JoinPoint;JoinPoint参数的值时由框架赋的,必须是第一位的参数@Before(value = "execution(public void com.spring.aop.ba01.SomeServiceImpl.doSome(String,Integer))") public void myBefore2(JoinPoint jp){ //获取方法的完整定义 System.out.println("方法的签名(定义)="+jp.getSignature()); System.out.println("方法名称="+jp.getSignature().getName()); //获取方法的实参 Object o[]=jp.getArgs(); for (Object oo: o) { System.out.println("参数="+oo); } System.out.println("切面功能-前置通知0:在目标方法前执行"+new Date()); }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dwb0U9in-1595767961340)(https://ae01.alicdn.com/kf/H7325a1e6e7314028889aa6de6a7c68f72.jpg)]
注意:先完成下面的aspectj的实现再加入JoinPoint
-
Pointcut(切入点):连接点的集合,表示切面的位置
定义和管理切入点,当项目中有多个切入点表达式是重复的时候使用;自定义方法无需加入具体代码,只需加上@Pointcut注解和切入点表达式,其他地方要用到时,直接写自定义方法名即可
属性:
value 切入点表达式
例子:@Aspect public class MyAspect { @After(value = "mypt()") public void myAfter(){ System.out.println("最终通知:总是会被执行"); //一般做资源的清楚工作 } @Before(value = "mypt()") public void myBefore(){ System.out.println("前置通知:在目标方法前执行执行"); } @Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))") private void mypt(){ //无需具体代码 } }
-
Advice(通知):执行切面的时间,表示切面执行的时间
-
Target(目标):指代理的目标对象
-
Weaving(植入):指把增强代码应用到目标上,生成代理对象的过程
-
Proxy(代理):指生成的代理对象
-
Aspect(切面):要增强的功能,常见的功能有日志、事务、统计信息、参数检查和权限验证
AspectJ实现aop
表示切面的执行时间(Advice)的注解(也可以使用xml中的标签):
-
@Before
前置通知:下面有具体例子见下 -
@AfterReturning
后置通知:
属性:
1.value
:切入点表达式
2.returning
自定义变量,表示目标方法的返回值,自定义变量名必须和通知方法的形参名一致
位置:方法定义上
功能:在目标方法后执行,获取目标方法的返回值,可以根据这个返回值做不同的处理方法,可以修改这个返回值方法有参数,推荐用Object,参数名自定义
public class SomeServiceImpl implements SomeService {//目标类 @Override public String doOther(String name, Integer age) { System.out.println("这是目标类SomeService的doOther()..."); return "abcd"; } }
@Aspect public class MyAspect {//切面类 @AfterReturning(value ="execution(* *..SomeServiceImpl.doOther(..))" ,returning = "res") public void myAfterReturning(Object res){ //Object res:是目标方法执行后的返回值,根据返回值我们可以做相应的切面处理 System.out.println("后置通知,在目标方法后执行,获取的返回值是:"+res); } }
public class MyTest02 {//测试类 @Test public void test01(){ String con="applicationContext.xml"; ApplicationContext ac=new ClassPathXmlApplicationContext(con); SomeService ss= (SomeService) ac.getBean("someService"); ss.doOther("pp",2); //ss.doOther2("0",0); } }
-
@Around
环绕通知------最强的通知:public、有返回值(推荐用Object)、属性value:切入点表达式;方法名称自定义,方法有参数,固定参数:ProceedingJoinPoint
功能:
1、在目标方法前后执行
2、控制目标方法是否被调用
3、影响目标方法:修改目标方法的结果等同于jdk动态代理的InvocationHandler;参数=Method=执行目标方法;返回值=目标方法的执行结果,可修改
@Aspect public class MyAspect { @Around(value = "execution(* *..SomeServiceImpl.doFirst(..))") public Object myAround(ProceedingJoinPoint pjp) throws Throwable { Object result=null; System.out.println("环绕通知-执行前加时间功能=="+new Date()); //目标方法调用 等同于method.invoke(); Object result=doFirst() String nname=null; Object args[]=pjp.getArgs(); if(args!=null && args.length>1){ Object arg=args[0]; nname= (String) arg; } //判断什么条件执行业务方法 if(nname=="环绕小姐"){ result=pjp.proceed(); }else { System.out.println("你不是环绕小姐!!!"); }
System.out.println("环绕通知-执行后执行功能"); //在目标方法前后增强功能 if(result != null){ result="改变目标方法的返回结果"; } return result; }
}
-
@AfterThrowing
(少用)
异常通知:
属性:1、value切入点表达式
2、throwing自定义变量,表示目标方法抛出的异常对象,变量名必须和方法的参数一致
功能:
1、在目标方法抛出异常时执行
2、可以做目标方法的监控程序:有异常可以发右键、短信来通知
参数:本身有一个throwing,也可以再加一个JoinPoint
执行原理:try{ SomeServiceImpl.doSecond(..) }catch{Exception e{ myAfterThrowing(e) }
例子:
@Aspect public class MyAspect { @AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))",throwing = "ex") public void myAfterThrowing(Exception ex){ System.out.println("异常通知:程序出异常啦"+ex.getMessage()); //可以发邮件、短信... } }
记得要让业务方法产生异常才能看见效果
-
@After
(少用)
最终通知: public、void、方法名称自定义、无参(可加JoinPoint)
功能:总是会执行(在目标方法之后),哪怕程序报异常也输出
实现原理:try{ SomeServiceImpl.doThird(..) }catch(Exception e){ }finally{ myAfter() }
例子:
@Aspect public class MyAspect { @After(value = "execution(* *..SomeServiceImpl.doThird(..))") public void myAfter(){ System.out.println("最终通知:总是会被执行"); //一般做资源的清楚工作 } }
可以试试发生异常时这个方法是否执行~
使用切面表达式来表示切面的位置:
用切面表达式匹配目标方法的方法名,所以execution表达式就是方法的前面,表达式中每个部分用空格隔开,在表达式中可以使用通配符
1、*表示0~n个任意字符
2、…用在方法参数时,表示任意多个参数;用在包名后,表示当前包及其子包path
3、+用在类名后,表示当前类及其子类;用在接口后,表示当前接口及其实现类
例子:
execution(public * *(..))
表示公共任意方法execution(public * set*(..))
表示公共的以“set”开头的方法execution(public * xx.yy.*.*(..))
表示xx.yy下任意类的任意方法,不包括xx.yy的子包execution(public * xx.yy.zz.*.*(..))
表示xx.yy.zz下任意类的任意方法,包括xx.yy的* 子包execution(public * *..zz*.*(..))
表示所有包下任意类的任意方法
实现aspectj步骤(使用@Before):
- 创建maven项目
- 加入依赖
- spring依赖
- aspectj依赖
- junit依赖
- 创建目标类(需要增强方法):接口及其实现类
public class SomeServiceImpl implements SomeService {//目标类
@Override
public void doSome(String name, Integer age) {//在方法执行前输出当前时间
System.out.println("这是目标类SomeService的doSome()...");
System.out.println(name+"-->"+age);
}
}
- 创建切面类:普通类
- 在类上加
@Aspect
- 在类中定义方法(要执行的功能代码),在上面加通知注解,如
@Before
;有需要指定切入点表达式execution()
/**
* @Aspect:是aspectj框架中的注解
* 表示当前类为切面类
* 位于类定义的上面
*/
@Aspect
public class MyAspect {
/**
* 定义切面功能:即要增强的功能
* 要求:
* 1、public
* 2、void
* 3、名称自定义
* 4、参数有无均可:
* 有参时:参数非自定义,有几个参数类型可以使用
*/
/**
*@Before:前置通知注解
* 属性value:切入点表达式,表明增强功能的执行位置
* 位于方法定义上
* 功能:让增强功能在目标功能前执行并且不影响目标方法的执行
* 注意:方法中的参数只需写上参数类型,无需写参数名 doSome(String,Integer)
*/
@Before(value = "execution(public void com.spring.aop.ba01.SomeServiceImpl.doSome(String,Integer))")
public void myBefore(){
System.out.println("切面功能-前置通知:在目标方法前执行"+new Date());
}
//极简版,可以有多个增强功能
@Before(value = "execution(* *..SomeServiceImpl.do*(..))")
public void myBefore1(){
System.out.println("切面功能-前置通知1:在目标方法前执行"+new Date());
}
}
- 创建spring配置文件:声明对象,把对象的创建交给容器管理(声明对象可用注解/xml配置文件)
- 声明目标对象
- 声明切面类对象
- 声明aspectj的自动代理生成器标签(完成代理对象的自动创建)
<!--目标对象-->
<bean id="someService" class="com.spring.aop.ba01.SomeServiceImpl"/>
<!--切面对象-->
<bean id="myAspect" class="com.spring.aop.ba01.MyAspect"/>
<!--
自动代理生成器:使用aspectj框架内部的功能,创建目标的代理对象
其过程时在内存中实现的,修改目标对象在内存中的结构,创建为代理对象
so,目标对象就是被修改后的代理对象
aspectj-autoProxy会把容器中所有目标对象一次性都生成代理对象
-->
<aop:aspectj-autoproxy/>
- 创建测试类,从容器中获取目标对象(即代理对象),通过代理执行方法,实现aop功能增强
public class MyTest01 {
@Test
public void test01(){
String con="applicationContext.xml";
ApplicationContext ac=new ClassPathXmlApplicationContext(con);
SomeService ss= (SomeService) ac.getBean("someService");
ss.doSome("tt",1);
}
}
jdk动态代理
指不改动原先的类,对该类进行增强,实现新的功能;要求目标对象必须实现接口;java通过java.lang.reflect包提供三个类支持代理模式Proxy、Method和InovcationHandler
实现步骤:
- 创建目标类
public class SomeServiceImpl implements SomeService {
@Override
public void doSome() {
//ServiceTools.doLog();
System.out.println("执行业务方法doSome()");
}
@Override
public void doOther() {
//ServiceTools.doLog();
System.out.println("执行业务方法doOther()");
}
}
//这是要加入的新功能
public class ServiceTools {
public static void doLog(){
System.out.println("现在的时间=="+new Date());
}
public static void doLove(){
System.out.println("收尾功能--");
}
}
- 创建InvocationHandler接口的实现类,在这个类中实现目标方法(增加功能)
public class MyInvocationHandler implements InvocationHandler {
//目标对象
private Object target;
//要写构造方法,不然调用时,没有目标对象传进来
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object res=null;
//主方法前的显示时间方法
System.out.println("method的名称:"+method.getName());
//要求doSome()才有新增的方法
String methodName=method.getName();
if("doSome".equals(methodName)){
ServiceTools.doLog();
//通过Mehod类来执行目标类的方法:doSome()、doOther
res=method.invoke(target,args);
//主方法后的收尾方法
ServiceTools.doLove();
}else{
res=method.invoke(target,args);
}
return res;
}
}
- 使用jsk的Proxy类,创建代理对象,实现创建对象的能力
public class MyApp {
public static void main(String[] args) {
//创建目标对象
SomeService target=new SomeServiceImpl();
//创建InvocationHandler对象,注意要传入目标对象
InvocationHandler ih=new MyInvocationHandler(target);
//参数 类加载器、实现接口、目标代理类
SomeService proxy= (SomeService) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),ih);
//调用方法
proxy.doSome();
System.out.println("===================");
proxy.doOther();
}
}
CGLIB动态代理
要求目标类能被继承,即不能是final类
关于代理
- 当目标类是实现接口时,spring默认用的是jdk的动态代理
- 当目标类没有实现接口时,spring会自动转换成cglib框架进行动态代理
- 当目标类实现了接口,但是又想让项目用cglib框架时,只需把xml配置文件中的
<aop:aspectj-autoproxy />
改成<aop:aspectj-autoproxy proxy-target-class="true"/>
即可