Spring Aop 详解一
共 8990字,需浏览 18分钟
·
2020-10-20 13:42
点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
作者 | 梦想家haima
来源 | urlify.cn/7nymqu
Aop 是一个编程思想,最初是一个理论,最后落地成了很多的技术实现。
我们写一个系统,都希望尽量少写点儿重复的东西。而很多时候呢,又不得不写一些重复的东西。比如访问某些方法的权限
,执行某些方法性能的日志
,数据库操作的方法进行事务控制
。以上提到的,权限的控制,事务控制,性能监控的日志 可以叫一个切面。像一个横切面穿过这一些列需要控制的方法
。通过aop编程,实现了对切面业务的统一处理。
以上是我对aop的一个总体概括
aop的原始实现
通过动态代理和反射实现,又称之为JDK动态代理
MyInterceptor.java
package demo.aop.jdkproxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* 拦截器
* 1、目标类导入进来
* 2、事务导入进来
* 3、invoke完成
* 1、开启事务
* 2、调用目标对象的方法
* 3、事务的提交
* @author zd
*
*/
public class MyInterceptor implements InvocationHandler{
private Object target;//目标类
private Transaction transaction;
public MyInterceptor(Object target, Transaction transaction) {
super();
this.target = target;
this.transaction = transaction;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
String methodName = method.getName();
if("savePerson".equals(methodName)||"updatePerson".equals(methodName)
||"deletePerson".equals(methodName)){
this.transaction.beginTransaction();//开启事务
method.invoke(target);//调用目标方法
this.transaction.commit();//事务的提交
}else{
method.invoke(target);
}
return null;
}
}
PersonDao.java
package demo.aop.jdkproxy;
public interface PersonDao {
public void savePerson();
public void updatePerson();
}
PersonDaoImpl.java
package demo.aop.jdkproxy;
public class PersonDaoImpl implements PersonDao{
public void savePerson() {
System.out.println("save person");
}
public void updatePerson() {
// TODO Auto-generated method stub
System.out.println("update person");
}
}
Transaction.java
package demo.aop.jdkproxy;
public class Transaction {
public void beginTransaction(){
System.out.println("begin transaction");
}
public void commit(){
System.out.println("commit");
}
}
JDKProxyTest
package demo.aop.jdkproxy;
import org.junit.Test;
import java.lang.reflect.Proxy;
/**
* 1、拦截器的invoke方法是在时候执行的?
* 当在客户端,代理对象调用方法的时候,进入到了拦截器的invoke方法
* 2、代理对象的方法体的内容是什么?
* 拦截器的invoke方法的内容就是代理对象的方法的内容
* 3、拦截器中的invoke方法中的参数method是谁在什么时候传递过来的?
* 代理对象调用方法的时候,进入了拦截器中的invoke方法,所以invoke方法中的参数method就是
* 代理对象调用的方法
* @author zd
*
*/
public class JDKProxyTest {
@Test
public void testJDKProxy(){
/**
* 1、创建一个目标对象
* 2、创建一个事务
* 3、创建一个拦截器
* 4、动态产生一个代理对象
*/
Object target = new PersonDaoImpl();
Transaction transaction = new Transaction();
MyInterceptor interceptor = new MyInterceptor(target, transaction);
/**
* 1、目标类的类加载器
* 2、目标类实现的所有的接口
* 3、拦截器
*/
PersonDao personDao = (PersonDao) Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), interceptor);
//personDao.savePerson();
personDao.updatePerson();
}
}
运行结果
begin transaction
update person
commit
原始实现部分,想必现在很少会有人再这么写了。但这个对于我们理解Aop的思想很有帮助。
我们可以看到 代理对象 personDao调用的方法
updatePerson
中没有模拟事务的代码,但最终代理对象却输出了begin transaction
和commit
Spring AOP概念核心词
切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用基于模式)或者基于@Aspect注解的方式来实现。
连接点(Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是表示一个方法的执行。
通知(Advice):在切面的某个特定的连接点上执行的动作。其中包括了“around”、“before”和“after”等不同类型的通知(通知的类型将在后面部分进行讨论)。许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。
切入点(Pointcut):匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。
引入(Introduction):用来给一个类型声明额外的方法或属性(也被称为连接类型声明(inter-type declaration))。Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用引入来使一个bean实现IsModified接口,以便简化缓存机制。
目标对象(Target Object):被一个或者多个切面所通知的对象。也被称做被通知(advised)对象。既然Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。
AOP代理(AOP Proxy):AOP框架创建的对象,用来实现切面契约(例如通知方法执行等等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。
上面为官方文档,有的地方还是很难读懂,毕竟是纯概念。下面我用自己的话来翻译一下,如果有不对的地方,请指正
切面 统一处理的业务,比如上文提到的 权限控制,事务处理
连接点 原本被执行的方法,一个执行的方法可能被多个切面横切
通知 切面方法的执行,比如权限控制的具体执行过程(权限控制可以用前置通知@Before)
切入点 切入点的概念通常和连接点概念容易分不清,切入点其实是一个规则,也就是说什么样的情况下(满足什么规则),
就会去执行链接点的那些方法,这个规则就是切入点,这种规则用切入点表达式去制定引入(Introduction) 被代理的对象可以引入新接口,通过默认的实现类,让这个被代理的类增强
目标对象 就是被切面执行了的对象
AOP代理 代理包括jdk代理和cglib代理,是aop底层实现过程
织入 就是切面中的方法完成加载执行的过程
这里有8个概念,但真正要完成aop的理解,还不得不再引入两个概念。
被代理对象 我们可以看到,上面说到目标对象
永远是一个被代理的对象
,也是被通知的对象。代理对象 代理对象呢, 就是最后通知后,生成的对象。
切入点表达式
execution
用于匹配指定类型内的方法执行,匹配的是方法,可以确切到方法
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern (param-pattern)
throws-pattern?)
modifiers-pattern 修饰符表达式 :public protect private ,可缺省,表示不限制
ret-type-pattern 返回值表达式 如 String代表返回值为String ,*代表任意返回值都可以 必填字段
declaring-type-pattern 类型,可以由完整包名加类名组成 可以只写包名加.*限定包下的所有类 可缺省,表示不限制
name-pattern 方法名表达式,可以由*统配所有字符 必填字段
param-pattern 参数列表,可以用..来表示所有的方法 必填字段
execution(public * * (..)) //所有public的方法
execution(* set*(..)) //所有set开头的方法
execution( * com.xyz.service.AccountService.* (..) ) //AccountService的所有方法,如果AccountService是接口,指实现了这个接口的所有方法
execution(* com.xyz.service.*.*(..)) //com.xyz.service包下的所有类的所有方法
execution(* com.xyz.service..*.*(..)) //com.xyz.service包及**子包**下的所有类的所有方法
within
用于匹配指定类型内的方法执行,匹配的是类型内的方法,类型下的所有方法
within (com.xyz.service.*) // com.xyz.service包下面的所有类的所有方法
within (com.xyz.service..*) // com.xyz.service包及**子包**下面的所有类的所有方法
within (com.xyz.service.impl.UserServiceImpl) // UserServiceImpl类下面所有方法
this
用于匹配当前AOP代理对象类型的执行方法,在前文中引入(Introduction)
的代理对象使用,可以注入代理对象bean
指定spring容器中特定名称的bean的所有方法为连接点target
用于匹配当前目标对象类型的执行方法,可以注入目标对象,被代理的对象args
用于匹配当前执行的方法传入的参数为指定类型的执行方法,可以注入连接点(方法)的参数列表@target 暂未解读
@args 暂未解读
@within 暂未解读
@annotation 暂未解读
代码实战
5种通知的案例
DemoAspect.java
定义切面,及通知,这里为了测试更多的案例,表达式切到AdviceKindTestController.java
package demo.aop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Slf4j
@Component //必须是个bean
public class DemoAspect {
//前置通知
@Before("execution (* demo.aop.controller.AdviceKindTestController.*(..))")
public void auth() {
log.info("前置通知,假装校验了个权限");
}
//后置通知
@AfterReturning("execution (* demo.aop.controller.AdviceKindTestController.*(..))")
public Object afterSomething(){
log.info("后置通知,不太清楚运用场景");
return "ok";
}
//环绕通知
//如果环绕通知 不返回执行结果 方法不会返回任何结果,导致接口拿不到任何数据
//所以一定把proceed 返回
//ProceedingJoinPoint 是 JoinPoint的子类,仅当环绕通知的时候,可以注入ProceedingJoinPoint的连接点
@Around("execution (* demo.aop.controller.AdviceKindTestController.*(..))")
public Object getMethodTime(ProceedingJoinPoint point) throws Throwable {
log.info("环绕通知,统计方法耗时,方法执行前");
Long beforeMillis = System.currentTimeMillis();
Object proceed = point.proceed();
Long taketimes= System.currentTimeMillis()-beforeMillis;
log.info(String.format("该方法用时%s毫秒",taketimes));
return proceed;
}
//异常通知
@AfterThrowing("execution (* demo.aop.controller.AdviceKindTestController.*(..))")
public void throwSomething() {
log.info("异常通知,只有异常了才会通知。具体场景,不是特别了解");
}
//最终通知
@After("execution (* demo.aop.controller.AdviceKindTestController.*(..))")
public void closeSomething() {
log.info("最终通知,官网说,可以用来回收某些资源。无论发不发生异常,都会被执行");
}
}
AdviceKindTestController.java
测试用的接口类
package demo.aop.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AdviceKindTestController {
@GetMapping("/advice") //http://localhost:8080/advice
public String test() throws InterruptedException {
Thread.sleep(4);
return "ok";
}
@GetMapping("/advice/throwing") //http://localhost:8080/advice/throwing
public String test2(){
int i=1/0;
return "ok";
}
}
访问 http://localhost:8080/advice 后台输出为
2020-10-18 11:24:01.201 : 环绕通知,统计方法耗时,方法执行前
2020-10-18 11:24:01.201 : 前置通知,假装校验了个权限
2020-10-18 11:24:01.201 : 该方法用时6毫秒
2020-10-18 11:24:01.202 : 最终通知,官网说,可以用来回收某些资源。无论发不发生异常,都会被执行
2020-10-18 11:24:01.202 : 后置通知,不太清楚运用场景
执行顺序
- 环绕通知的前面部分
- 前置通知
- 环绕通知的后面部分
- 最终通知
- 后置通知
访问 http://localhost:8080/advice/throwing 后台输出为
2020-10-18 11:30:34.935 : 环绕通知,统计方法耗时,方法执行前
2020-10-18 11:30:34.935 : 前置通知,假装校验了个权限
2020-10-18 11:30:34.936 : 最终通知,官网说,可以用来回收某些资源。无论发不发生异常,都会被执行
2020-10-18 11:30:34.936 : 异常通知,只有异常了才会通知。具体场景,不是特别了解
java.lang.ArithmeticException: / by zero //test2()方法抛出了异常
执行顺序如下,我们可以看到因为接口出现了异常,所以后置通知并没执行,环绕通知的后面部分也没执行,但最终通知
和异常通知
被执行
- 环绕通知的前面部分
- 前置通知
- 最终通知
- 异常通知
粉丝福利:108本java从入门到大神精选电子书领取
???
?长按上方锋哥微信二维码 2 秒 备注「1234」即可获取资料以及 可以进入java1234官方微信群
感谢点赞支持下哈