AOP
代理
在有些情况下,一个客户不能或者不想直接访问另一个对象,这时需要找一个中介帮忙完成某项任务,这个中介就是代理对象。例如,购买火车票不一定要去火车站买,可以通过 12306 网站或者去火车票代售点买。又如找女朋友、找保姆、找工作等都可以通过找中介完成。
设计模式中的代理模式 http://c.biancheng.net/view/1359.html
JDK动态代理
需要被代理者是接口 或 有实现的接口,而要增强的是接口中定义的方法。使用Proxy类的静态方法newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler handler)
来动态生成代理对象
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.function.BiConsumer;
public class ProxyTest {
public static void main(String[] args) {
// 使用BiCosumer定义一个函数式的变量,传入到向代理对象中,用来动态指定增强方法
BiConsumer before = (x, y) -> {
String msg = ((Method) x).getName() + " 方法调用前" + y;
System.out.println(msg);
};
BiConsumer after = (x, y) -> {
String msg = ((Method) x).getName() + " 方法调用后,结果为:" + y;
System.out.println(msg);
};
// 获取代理对象
UserService us = (UserService) getProxy(new UserServiceImpl(), before,after);
// 使用代理对象执行方法
us.addUser();
us.addUser();
}
// 封装获取动态代理对象的方法
public static Object getProxy(Object target, BiConsumer beforeMethod, BiConsumer afterMethod) {
//需要传入的参数是:目标对象的类加载器、目标对象的接口的类类型、调用处理程序
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
// 调用处理程序是一个接口,需要实现其中的invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
// 目标方法执行前要执行的方法,就是增强的方法
beforeMethod.accept(method, args);
// 真正执行目标方法
Object result = method.invoke(target, args);
// 目标方法执行后要执行的方法,就是增强的方法
afterMethod.accept(method, result);
return result;
}
});
}
}
CGlib代理模式
被代理者有无接口都可以,代理对象是目标对象的子类.步骤:先创建Enhancer对象,并设置其父类为目标对象,然后设置回调方法,即目标方法的环绕方法。
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibProxyTest {
public static void main(String[] args) {
UserService us = (UserService) getProxy(new UserServiceImpl());
us.addUser();
}
public static Object getProxy(Object target){
Enhancer enhancer = new Enhancer();
// 把目标对象设置为代理对象的父类
enhancer.setSuperclass(target.getClass());
// 设置目标方法执行时的环绕方法
enhancer.setCallback(
// 需要传入Callback类型的变量,目前老师讲的是使用这个接口(它继承了Callback)
new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println(method.getName()+" 方法被调用了");
Object result = method.invoke(target, objects);
System.out.println(method.getName()+" 方法调用完了");
return result;
}
}
);
return enhancer.create();
}
}
AOP注解配置方式
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
总结: Spring中的AOP 利用代理对象在不修改源代码的条件下,对方法进行扩展.
概念扫盲
5.1. AOP 概念
让我们首先定义一些核心的 AOP 概念和术语。这些术语不是特定于 Spring 的。不幸的是,AOP 术语并不是特别直观。但是,如果 Spring 使用自己的术语会更加混乱。
Aspect
:跨越多个类的关注点的模块。事务管理是企业 Java 应用程序中横切关注点的一个很好的例子。在 Spring AOP 中,Aspect
是通过使用常规类(基于模式的方法)或使用@Aspect
注解注解的常规类 (@AspectJ 风格)实现的。
Join point
:程序执行过程中的一个点,例如方法的执行或异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法的执行。
Advice
:Advice
在特定连接点采取的行动。不同类型的Advise
包括“周围”、“之前”和“之后”建议。(通知类型将在后面讨论。)许多 AOP 框架,包括 Spring,将通知建模为拦截器,并在连接点周围维护一个拦截器链。
Pointcut
:匹配连接点的谓词。Advice 与Pointcut
表达式相关联,并在与切入点匹配的任何连接点处运行(例如,执行具有特定名称的方法)。由Pointcut
表达式匹配的连接点的概念是 AOP 的核心,Spring 默认使用 AspectJ 切入点表达式语言。
Introduction
:代表类型声明额外的方法或字段。Spring AOP 允许您向任何建议的对象引入新的接口(和相应的实现)。例如,您可以使用介绍使 bean 实现 IsModified接口,以简化缓存。(介绍在 AspectJ 社区中称为类型间声明。)
Target object
:被一个或多个方面建议的对象。也称为“建议对象”。由于 Spring AOP 是使用运行时代理来实现的,所以这个对象始终是一个被代理的对象。
AOP proxy
:由 AOP 框架创建的对象,用于实现方面契约(建议方法执行等)。在 Spring Framework 中,AOP 代理是 JDK 动态代理或 CGLIB 代理。
Weaving
:将切面与其他应用程序类型或对象联系起来,以创建建议对象。这可以在编译时(例如,使用 AspectJ 编译器)、加载时或运行时完成。Spring AOP 与其他纯 Java AOP 框架一样,在运行时执行编织。
注解开发:
准备
首先pom.xml文件里面需要添加aop的依赖
<!--引入AOPjar包文件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
5.4. @AspectJ support https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-ataspectj
- 首先在配置类里需要添加
@EnableAspectJAutoProxy
注解来开启动代理功能
@Configuration
@ComponentScan("com.jt")
@EnableAspectJAutoProxy // 添加此注解开启切面的支持
class MySpringConfig{}
Aspect切面
创建一个切面处理类,并使用@Aspect
注解标识此类为一个切面,并且还要添加@Component
注解,以声明此类需要Spring容器进行管理,准备好切面类后,就可以在此类里面定义切入点和通知了
@Component
@Aspect
class PointClass {}
PointCut切入点
- 定义
切入点可以确定我们所需要的连接点,从而使我们能够控制通知何时运行。Spring AOP 仅支持 Spring bean 的方法执行连接点,因此您可以将切入点视为匹配的 Spring bean 上方法的执行。一个切入点声明有两个部分:一个包含名称和任何参数的签名和一个切入点表达式,它确定我们对哪些方法执行感兴趣。在 AOP 的@AspectJ 注释样式中,一个切入点签名由常规方法定义提供, 切入点表达式使用@Pointcut注解表示(作为切入点签名的方法必须有void返回类型)。@Pointcut("bean(beanId)")
使用此种形式来指定要为哪个beanId所指向的bean的所有方法添加当前切面
切入点表达式书写方法详见官方文档5.4.3. 声明一个切入点 https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-pointcuts
// 注解后面括号里面的意思是对beanId为“userServiceImpl”的bean来执行切面里面的方法
@Pointcut("bean(userServiceImpl)") //切入点表达式
public void pointCutHandler() {} // 切入点签名
-
winthin
标签@Pointcut("winthin(类的全限定路径名)")
,指定要拦截的具体的单个类
@Pointcut("winthin(com.jt.service.UserServiceImpl") //切入点表达式
public void pointCutHandler() {} // 切入点签名
@Pointcut("winthin(*)")
使用通配符com.jt.service.*
此方式只能通配service包下的类,子包、孙包等里面的类就匹配不到了com.jt.service..*
此方式可以通配service包、子包、孙包等下面的所有的类com.*.service..*
com包下所有子包是service的,service包下的类com..*.service..*
com包下,所有service子包下的所有子类
@Pointcut("winthin(com.jt.service.*") //
public void pointCutHandler() {} // 切入点签名
-
execution
表达式,粒度比较细,可以按照方法签名来匹配,执行表达式的格式如下:
表达式结构
execution(modifiers-pattern? return-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
非红色部分为可选项
All parts except the returning type pattern (ret-type-pattern in the preceding snippet), the name pattern, and the parameters pattern are optional. The returning type pattern determines what the return type of the method must be in order for a join point to be matched. * is most frequently used as the returning type pattern. It matches any return type. A fully-qualified type name matches only when the method returns the given type. The name pattern matches the method name. You can use the * wildcard as all or part of a name pattern. If you specify a declaring type pattern, include a trailing . to join it to the name pattern component. The parameters pattern is slightly more complex: () matches a method that takes no parameters, whereas (…) matches any number (zero or more) of parameters. The () pattern matches a method that takes one parameter of any type. (,String) matches a method that takes two parameters. The first can be of any type, while the second must be a String. Consult the Language Semantics section of the AspectJ Programming Guide for more information.
表达式 | 意义 |
---|---|
execution(* com.jt.service.UserServiceImpl.adduser() |
精确匹配此方法,但不限定其权限 |
execution(* com.jt.service..*.*(..) |
匹配com.jt.service包下所有类的包含任意参数的所有方法,且不限定权限 |
execution(public * *(..)) |
匹配所有public 权限的方法 |
execution(* set*(..)) |
匹配任意参数列表的、set开头的方法 |
execution(* com.xyz.service.AccountService.*(..)) |
AccountService类或接口下的任意参数的所有方法,不限权限 |
execution(* com.xyz.service.*.*(..)) |
service包下所有一级子类的任意参数列表的所有方法 |
execution(* com.xyz.service..*.*(..)) |
service包下所有子类(含子包)的任意参数列表的所有方法 |
execution(* *..tedu.*.add*(..)) |
匹配任意包下的tedu包下的所有子类的add开头的、任意参数列表的方法 |
-
annotation
表达式@annotation(org.springframework.transaction.annotation.Transactional)
所有标了@Transactional
注解的方法,Any join point (method execution only in Spring AOP) where the executing method has an @Transactional annotation: -
您可以使用
&&
,||
和组合切入点表达式!
。您还可以按名称引用切入点表达式
@Pointcut("@annotation(com.jt.annotation.Animal) || execution(* *..tedu.*.add*(..))")
public void pointcut(){}
- 如果是通过注解来匹配方法,然后方法中又要使用该注解时,可以定义一个形参用来接收此实参
@Around("@annotation(pri)")
public Object around1(ProceedingJoinPoint joinPoint,Pri pri) throws Throwable {
String name = pri.name();
System.out.println(name);
return joinPoint.proceed();
}
Advise通知
- 第一类通知,前置、后置、返回结果后、抛出异常后,此类通知不会影响目标方法的执行,因此一般被应用于日志系统。
-
@Before("pointCutHandler()")
标识前置方法,即在目标方法执行前执行的方法,这里的pointCutHandler是指当前切面类中切入点的签名,也可以直接写切入点表达式
注意:如果只定义了切入点,但未定义任何通知的情况下,通过容器得到的对象还是原对象,否则会得到代理对象
@Before("pointCutHandler()")
public void beforeMethod() {
System.out.println("目标方法前置执行点...");
}
-
@After("pointCutHandler()")
标识后置方法,即在目标方法执行后执行的方法,这里的pointCutHandler同上
@After("pointCutHandler()")
public void afterMethod() {
System.out.println("目标方法后置执行点...");
}
-
@AfterReturning("pointCutHandler()")
方法返回结果后执行的方法,注意此方法优先于@After()
注解执行
@AfterReturning("pointCutHandler()")
public void afterReturningMethod() {
System.out.println("目标方法invoke完成有结果后执行点...");
}
@AfterReturning
注解里面有一个returning
属性,可以把目标方法的返回结果注入到该方法的形参里面
Returns:the name of the argument in the advice signature to bind the returned value to
@AfterReturning(value="@annotation(com.jt.annotation.Animal)",returning="haha")
public void afterReturningMethod(Object haha) {
System.out.println("目标方法invoke完成有结果后执行点...");
System.out.println("目标方法返回的结果:"+haha);
}
-
@AfterThrowing("pointCutHandler()")
方法在目标方法执行中遇到异常时执行
@AfterThrowing("pointCutHandler()")
public void afterThrowingMethod() {
System.out.println("目标方法执行过程中遇到异常时执行点...");
}
@AfterThrowing("pointCutHandler()")
注解里面还有一个throwing属性,用来向形参中注入异常对象
Returns:the name of the argument in the advice signature to bind the thrown exception to
@AfterThrowing(value = "pointcut()",throwing = "myException")
public void afterThrowing(Exception myException){
System.out.println("切入点抛出异常后触发的通知");
System.out.println("异常信息:"+myException.getMessage());
myException.printStackTrace();
}
- 第二类通知,只有一个
@Around
,可以控制目标方法执行与否,即可以影响业务流转的过程。常见的应用有:权限的校验、缓存系统、异常的处理
@Around("pointCutHandler()")
环绕方法此方法比较特殊,它可以接收一个参数,该参数是目标对象绑定了所有上面定义的各种通知之后的代理对象。就是说环绕编程是在代理对象创建以及绑定各种通知之后,又在外面包了一层。
@Around("pointCutHandler()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕前置执行点...");
Object result = joinPoint.proceed();
System.out.println("环绕后置执行点...");
return result;
}
@Order()
如果对同一个切入点定义了多个环绕通知,可以使用@Order(1)
注解来指定绑定顺序,值越小越先绑定,优先绑定的优先执行
可以作用到类上、方法上和属性上
几种通知的执行时机,此图里面的trycatch可能不是十分恰当,需要结合注释理解
@Around{
Object result;
aroundCode;// 环绕方法的前半部分代码
@Before(){}
try{
result = target.method();
@AfterReturning(){}// 如果执行方法过程中抛出了异常,此通知不会被执行
}catch(Exception e){
@AfterThrowing(){}// 如果执行方法过程中抛出了异常会执行此通知
throw e;
}finally{
@After(){}// 无论是否发生异常都会执行
}
aroundCode;// 环绕方法的后半部分代码,如果发生了异常则不再执行
return result;
}
总体代码
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
public class AOPTest {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(MySpringConfig.class);
UserService userService = context.getBean(UserService.class);
userService.addUser();
}
}
@Configuration
@ComponentScan("com.jt")
@EnableAspectJAutoProxy
class MySpringConfig{
}
interface UserService{
void addUser();
}
@Service
class UserServiceImpl implements UserService{
@Override
public void addUser() {
System.out.println("增加了一个用户");
}
}
@Component
@Aspect
class PointClass {
@Pointcut("bean(userServiceImpl)")
public void pointCutHandler() {
}
@Before("pointCutHandler()")
public void beforeMethod() {
System.out.println("目标方法前置执行点...");
}
@After("pointCutHandler()")
public void afterMethod() {
System.out.println("目标方法后置执行点...");
}
@AfterReturning("pointCutHandler()")
public void afterReturningMethod() {
System.out.println("目标方法invoke完成有结果后执行点...");
}
@AfterThrowing("pointCutHandler()")
public void afterThrowingMethod() {
System.out.println("目标方法执行过程中遇到异常时执行点...");
}
@Around("pointCutHandler()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕前置执行点...");
Object result = joinPoint.proceed();
System.out.println("环绕后置执行点...");
return result;
}
}
通知中常用API
-
JoinPoint
通知方法中可以接收一个JoinPoint
类型的参数,用来获取下面的各种值,用应用于@Before
、@After
方法 | 值 | 解释 |
---|---|---|
getArgs() | [Ljava.lang.Object;@18cc679e |
Object 类型的数组 |
getKind() | method-execution | |
getSignature() | void com.jt.service.UserServiceImpl.addUser() | 方法签名,可以强转为MethodSignature,然后.getMethod()就可以获得反射中的Method对象了 |
getSourceLocation() | org.springframework.aop.aspectj. MethodInvocationProceedingJoinPoint$SourceLocationImpl@551a20d6 | |
getStaticPart() | execution(void com.jt.service.UserServiceImpl.addUser()) | |
getTarget() | com.jt.service.UserServiceImpl@578524c3 | 获得被代理对象,有可能返回null |
getThis() | com.jt.service.UserServiceImpl@578524c3 | 获得当前代理对象 |
toLongString() | execution(public void com.jt.service.UserServiceImpl.addUser()) | 切入点表达式 |
toShortString() | execution(UserServiceImpl.addUser()) | 切入点表达式 |
toString() | execution(void com.jt.service.UserServiceImpl.addUser()) | 切入点表达式 |