aop到底能做什么呢?
如:权限控制
、缓存控制
、事务控制
、审计日志
、性能监控
、分布式追踪
、异常处理
、数据认证
都可以使用aop; 为什么这些可以使用aop呢?
这一部分功能他与业务没有啥关系,但是他们的公用性非常的强,不管啥操作、啥业务,可能都需要这些;举个很常用的例子,当我们做java web开发的时候,如果我想打印出所有接口请求及响应的数据日志,我们要怎么打?这个关注点(通用需求)它与业务无关,任何业务、任何请求都得打;那我们要怎么做?在每个Controller的每一个方法里面都去打印?可是可以,但是这很显然是一个苦逼、无聊且没有任何营养的体力活;这个时候,我们就可以通过AOP去完美的解决这个问题,具体怎么做,后面再说。当我们剥开aop的外衣的时候,其实他的核心设计思想就是代理模式
;spring中大量用到了代理模式;如果你不太了解代理模式,其实也不影响你对aop的使用;这里我举个生活中的例子,带你了解一下什么代理模式;当你在美团、饿了么点餐的时候,其实就是一个典型的代理模式,美团(代理对象
)代理了餐馆(目标对象
)将美食(方法
)卖给你,同时对你的消费进行了增强
(帮你配送、送你优惠券等);帮你配送、送你赠品并不是餐厅做的;而是美团(代理对象)做的;但是这一切并没有影响到你就餐、也没有影响餐厅对商品的销售;aop同样也使用的这个方式,在不影响目标对象的前提下对他的功能进行增强。
Spring AOP只是引用了AspectJ相关的注解,借鉴了他的编程风格,具体的实现是由Spring自己做的,并没有依赖AspectJ编译器,因此核心的东西他们没有直接的关系。spring和aspect在实现上还存在本质上的区别,spring是在运行时动态生成的代理对象,aspect是在编译的时候就生成了代理对象;所以同样的面容,不同的灵魂...
对多个类的一个关注点的表达
;什么是关注点,通俗一点就是说,大家都要做或者都关心的事情;比如说:系统的权限校验,可能登录、下单、操作都涉及到用户权限的校验,那么权限校验就是关注点,在切面中,要明确的表达出切入的点、切入的时机,织入什么等等!。所以切面是对以下所有知识点的一个综合的理解。表达式
匹配出来的某一类(些)连接点(方法)的集合;切入点和连接点(Join point)是相辅相成的;简单理解就是,切入点通过表达式描述来匹配出对应的方法(连接点);
/**
* 所有的public方法
* 以下public * *(..)这个表达式匹配到的所有方法 都称之为连接点
*/
@Pointcut("execution(public * *(..))")
public void ex_pc1() {
}
Before
(之前)、After
(之后)、AfterReturning
(成功执行之后)、AfterThrowing
(执行异常)、Around
(环绕,该执行时机可以包含前面四种执行时机);实例如下:
/**
* Before的执行时机是在方法执行之前执行
* 这里可以通过运算符去匹配多个切入点
*/
@Before("ex_pc1() || ex_pc2()")
public void advice1() {
System.out.println("aop before..........");
}
JDK动态代理
或者CGLIB代理
增强
基于切入点
在特定的时机
作用于连接点
的过程叫做织入;
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
// 这里使用的是5.1.x的spring源码进行的测试
// 由于spring源码是使用的gradle构建的 所以下面是gradle的引入
compile(project(":spring-aop"))
compile group: 'org.aspectj', name: 'aspectjweaver', version: '1.9.6'
// 自定义注解
package com.lupf.aop.anno;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Lupf {
}
// ==========================================================
// 业务测试bean
package com.lupf.aop.service;
import com.lupf.aop.anno.Lupf;
@Lupf
public class BusiBean {
}
// ==========================================================
// 接口I
package com.lupf.aop.service;
public interface I {
public String busi1();
public void busi2(String arg1,Integer arg2);
}
// ==========================================================
// ServiceA
package com.lupf.aop.service.dir;
import com.lupf.aop.anno.Lupf;
import com.lupf.aop.service.BusiBean;
import org.springframework.stereotype.Component;
@Lupf
@Component
public class ServiceA {
public String busi1(BusiBean busiBean){
System.out.println("ServiceA busi1.....");
return "bbb";
}
public void busi2(@Lupf String arg1){
System.out.println("ServiceA busi2.....");
}
}
// ==========================================================
// Service B
package com.lupf.aop.service;
import com.lupf.aop.anno.Lupf;
import org.springframework.stereotype.Component;
@Component
public class ServiceB implements I{
@Override
@Lupf
public String busi1(){
System.out.println("ServiceB busi1.....");
//System.out.println(1/0);
return "bbb";
}
@Override
public void busi2(String arg1,Integer arg2){
System.out.println("ServiceB busi2.....");
}
}
// ==========================================================
// 配置类
package com.lupf.aop;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
@Component
@ComponentScan("com.lupf.aop")
@EnableAspectJAutoProxy//(proxyTargetClass = true)
public class AppConfig {
}
// ==========================================================
// 项目启动类 及测试代码
package com.lupf.aop;
import com.lupf.aop.service.I;
import com.lupf.aop.service.dir.ServiceA;
import com.lupf.aop.service.ServiceB;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
ac.register(AppConfig.class);
ac.refresh();
ServiceA serviceA = ac.getBean(ServiceA.class);
serviceA.busi1(null);
serviceA.busi2("a");
I serviceB = ac.getBean(I.class);
System.out.println(serviceB.busi1());;
serviceB.busi2("a",);
}
}
com.lupf.aop
的目录,将上面的代码按一下的目录结构拷贝到项目中即可用于测试了。*
(用于匹配任意字符)..
(匹配指定类及其子类,类似于java的泛型)+
(表达任意数的子包或者参数) 通配符操作,因为有时候我们希望通过一个切面去切一类的方法,如果我们逐一去写的话,就会显的很麻烦,因此使用通配符的话会大大降低我们的代码量用来表达希望同时满足多个条件的表达式
&&
(与-两个都必须满足)||
(或) 有一个满足就行!
(非) 取反主要用于描述,通过什么样的方式去匹配你想要操作的类对应的方法;
@Component // 交由spring管理
@Aspect // 指定当前类为一个aop切面
@Order(0) // 当有多个切面的时候,指定执行顺序,越小优先级越高
public class AspectJDemo {
// 写测试用例
}
exection
/**
* 完整的格式如下
* exection(
* modifier-pattern? // 修饰符 允许不配置
* ret-type-pattern // 返回值
* declaring-type-pattern? // 描述包名 允许不配置
* name-pattern(param-pattern) // 描述方法名 方法参数
* throws-pattern? // 匹配抛出的异常 允许不配置
* )
*/
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
// 以下为测试用例
/**
* 所有的public方法
*/
@Pointcut("execution(public * *(..))")
public void ex_pc1() {
}
/**
* 只有带了特定返回值的类型
*/
@Pointcut("execution( String *(..))")
public void ex_pc2() {
}
/**
* 指定指定的包名 带特定请求参数的
*/
@Pointcut("execution(public * com.lupf.aop..*(String,..))")
public void ex_pc3() {
}
/**
* 匹配执行的方法名 所以这里只有方法名为busi1的才会被代理
*/
@Pointcut("execution(public * com.lupf.aop..*1(..))")
public void ex_pc4() {
}
within
within为execution的一种简化模式 使用起来更便捷 但是他没有execution功能强大;其只能指明具体的类或具体目录下类中的方法
/**
* 指定具体的类ServiceB下的所有方法
*/
@Pointcut("within(com.lupf.aop.service.ServiceB)")
public void w_pc2(){
}
/**
* 通配service目录及子目录的所有类
* com.lupf.aop.service..* 会匹配当前目录及子目录
* com.lupf.aop.service.* 只会匹配当前目录 子目录不会去匹配
*/
@Pointcut("within(com.lupf.aop.service..*)")
public void w_pc1() {
}
args
匹配指定的参数
/**
* 指明包含特定入参的方法,可以通过..进行多个参数的统配
* args(String,Integer) 只能是第一个参数为String 第二个为Integer的才能匹配
* args(String,Integer,..) 只要是前两个参数为String和Integer的都可以匹配上
* args(..,Integer) 最后一个参数为Integer的都可以匹配上
*/
@Pointcut("args(String,Integer)")
public void arg_pc1() {
}
this
用于匹配代理对象的类;这一部分是相对不好理解的,可以参考注释详细的测试一下
/**
* 这里指明了com.lupf.aop.service.ServiceB用于匹配代理后的对象
* <p>
* 为了方便测试 这里定义了一个接口I ServiceB实现了I
* <p>
* 第一次测试,
* 使用 @EnableAspectJAutoProxy 表示默认使用jdk动态代理
* 那么spring获取对象是 I serviceB = ac.getBean(ServiceB.class)
* 这样serviceB对象调用方法的时候,不会被增强
* 原因是因为jdk代理是基于接口,那么生成的代理对象如下
* public $ServiceBByJDK implements I {
* publis ServiceB serviceB;
* public void busi1(){
* service.busi();
* }
*
* // other....
* }
* 所以$ServiceBByJDK无法匹配上ServiceB,增强失败
* <p>
* <p>
* ===============================================================
* <p>
* 第二次测试
*
* @EnableAspectJAutoProxy(proxyTargetClass = true) 强制使用cglib
* 那么就会得到一个基于cglib的代理实现
* 最终的代理对象效果如下
* public ServiceB$$EbhabcerBySpringCGLIB&&xxxx extend ServiceB implements I
* {
* // do something
* }
* 因为cglib代理是基于继承 因此生成的代理对象是ServiceB的子类,所以这里就可以匹配上
* 第二次测试执行的ServiceB中的所有方法都是可以增强的
*/
@Pointcut("this(com.lupf.aop.service.ServiceB)")
public void this_pc1() {
}
target
匹配动态代理之前 目标对象为指定对象的类
/**
* 由于是匹配的目标对象 因此就不会区分使用的什么代理方式了
* 因此jdk和cglib代理的对象都是可以增强的
*/
@Pointcut("target(com.lupf.aop.service.ServiceB)")
public void target_pc1() {
}
@within
匹配加了指定注解的类
/**
* 类上面加了指定注解,那么该类下所有的方法执行都增强
* <p>
* 注意:该方式只作用于类上面 如果注解只写在方法上没加在类上面,那么增强是无法生效的
*/
@Pointcut("@within(com.lupf.aop.anno.Lupf)")
public void anno_within_pc() {
}
@target
目标对象是否添加指定的注解
/**
* 代理对象的时候 目标对象添加了指定注解类的方法执行都会进行增强
*/
@Pointcut("@target(com.lupf.aop.anno.Lupf)")
public void anno_target_pc() {
}
@开头的都是作用于注解
@args
/**
* 方法参数带有指定注解的
* <p>
* 如下测试,定义一个测试对象
*
* @Lupf public class BusiBean {
* }
* <p>
* 如下的方法是会进行增强的
* public String busi1(BusiBean busiBean)
* <p>
* 下面这种方式是不会进行增强的
* public void busi2(@Lupf String arg1)
*/
@Pointcut("@args(com.lupf.aop.anno.Lupf)")
public void anno_args_pc() {
}
@annotation
加了指定注解的方法
/**
* 加了指定注解的方法调用的时候都会进行增强
* <p>
* 注意:这里只有方法,加在类上面是没有用的,和@within是对立的
*/
@Pointcut("@annotation(com.lupf.aop.anno.Lupf)")
public void anno_pc1() {
}
特别说明
需要匹配多个表达式的时候,可以使用运算符进行匹配
// 实例 表示添加了指定注解且只有一个String参数的方法
@Pointcut("@annotation(com.lupf.aop.anno.Lupf) && args(String)")
@Before
在目标方法执行之前执行增强的代码
/**
* Before的执行时机是在方法执行之前执行
* 这里可以通过运算符去匹配多个切入点
*/
@Before("w_pc1()")
public void advice1() {
System.out.println("aop before..........");
}
@AfterReturning
在目标方法成功执行
之后执行增强
/**
* 这个通知相对于After就更加明确了
* 只有在执行成功之后才会通知
* <p>
* 如果不需要管结果,那么我们只需要指定好切点就好了
* 如果我们需要拿到想要结果,就可以通过returning指定一个字段名称,并通过名称拿到响应数据
* 同时这里是可以指定具体的响应数据类型,当指定具体类型之后,就只有返回指定类型的数据才会触发通知
* <p>
* 这里是否需要返回 并不会影响到调用方
*
* @param re 响应数据
*/
// @AfterReturning("anno_pc1()")
@AfterReturning(pointcut = "anno_pc1()", returning = "re")
public void advice3(Object re) {
System.out.println("aop after reutrn........re:" + re);
}
@AfterThrowing
目标方法执行异常
之后执行增强
/**
* 当方法执行出现异常之后,就会触发这个通知
* <p>
* 如果不需要管异常是什么,那么我们只需要指定好切点就好了
* 如果我们需要拿到异常,就可以通过throwing指定一个字段名称,并通过名称拿到相应的异常
* 同时这里是可以指定具体的异常类型,当指定具体类型之后,就只有抛出指定类型的异常才会触发通知
*
* @param ex
*/
//@AfterThrowing("anno_pc1()")
@AfterThrowing(pointcut = "anno_pc1()", throwing = "ex")
public void advice4(Exception ex) {
System.out.println("aop after exception msg:" + ex.getMessage());
}
@After
目标方法不管执行成功还是失败
都会执行增强
/**
* 这个通知相对于After就更加明确了
* 只有在执行成功之后才会通知
* <p>
* 如果不需要管结果,那么我们只需要指定好切点就好了
* 如果我们需要拿到想要结果,就可以通过returning指定一个字段名称,并通过名称拿到响应数据
* 同时这里是可以指定具体的响应数据类型,当指定具体类型之后,就只有返回指定类型的数据才会触发通知
* <p>
* 这里是否需要返回 并不会影响到调用方
*
* @param re 响应数据
*/
// @AfterReturning("anno_pc1()")
@AfterReturning(pointcut = "anno_pc1()", returning = "re")
public void advice3(Object re) {
System.out.println("aop after reutrn........re:" + re);
}
@Around
环绕通知囊括了上面的4种情况,包含了所有的执行时机;所以这种方式是最常用的
/**
* 坏绕通知 囊括了上面4种通知时机
*
* @param joinPoint
* @return 响应数据
* @throws Throwable
*/
@Around("anno_pc1()")
public Object advice4(ProceedingJoinPoint joinPoint) throws Throwable {
//在这里相当于@Before
System.out.println("aop around before.........");
// 获取所有参数
Object[] args = joinPoint.getArgs();
try {
// 执行目标方法并获取响应对象
Object o = joinPoint.proceed(args);
// 到这里相当于@AfterReturning
System.out.println("aop around after returning.........");
// 返回数据
// 这里不同于@Before 这里如果不返回,调用方将拿不到响应数据
return o;
} catch (Throwable t) {
// 这里相当于@AfterThrowing
System.out.println("aop around after throwing.........");
throw t;
} finally {
// 到这里相当于@After 异常和正常只会出现其中一种情况
System.out.println("aop around after.........");
}
}
特别说明
,执行时机同样可以通过算术运算符匹配对各切入点,如:
@Before("w_pc1() && w_pc2()")
当多个切面作用于同一个方法的的时候,可以通过在切面类上加@Order(0)
来指定优先级,数越小,优先级越高。
package com.lupf.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component // 交由spring管理
@Aspect // 指定当前类为一个aop切面
@Order() // 当有多个切面的时候,指定执行顺序,越小优先级越高
public class AspectJDemo {
/**
* exection(
* modifier-pattern? // 修饰符 允许不配置
* ret-type-pattern // 返回值
* declaring-type-pattern? // 描述包名 允许不配置
* name-pattern(param-pattern) // 描述方法名 方法参数
* throws-pattern? // 匹配抛出的异常 允许不配置
* )
*/
/**
* 所有的public方法
* 以下public * *(..)这个表达式匹配到的所有方法 都称之为连接点
*/
@Pointcut("execution(public * *(..))")
public void ex_pc1() {
}
/**
* 只有带了特定返回值的类型
*/
@Pointcut("execution( String *(..))")
public void ex_pc2() {
}
/**
* 指定指定的包名 带特定请求参数的
*/
@Pointcut("execution(public * com.lupf.aop..*(String,..))")
public void ex_pc3() {
}
/**
* 匹配执行的方法名 所以这里只有方法名为busi1的才会被代理
*/
@Pointcut("execution(public * com.lupf.aop..*1(..))")
public void ex_pc4() {
}
// within 指明具体的类或者具体的包路径
// within 为execution的一种简化模式 使用起来更便捷 但是他没有execution功能强大
/**
* 指定具体的类ServiceB下的所有方法
*/
@Pointcut("within(com.lupf.aop.service.ServiceB)")
public void w_pc1() {
}
/**
* 通配service目录及子目录的所有类
* com.lupf.aop.service..* 会匹配当前目录及子目录
* com.lupf.aop.service.* 只会匹配当前目录 子目录不会去匹配
*/
@Pointcut("within(com.lupf.aop.service..*)")
public void w_pc2() {
}
/**
* 指明包含特定入参的方法,可以通过..进行多个参数的统配
* args(String,Integer) 只能是第一个参数为String 第二个为Integer的才能匹配
* args(String,Integer,..) 只要是前两个参数为String和Integer的都可以匹配上
* args(..,Integer) 最后一个参数为Integer的都可以匹配上
*/
@Pointcut("args(String,Integer)")
public void arg_pc1() {
}
// this 表示匹配代理对象
// spring aop 默认使用的jdk动态代理
// 可以在启动类添加@EnableAspectJAutoProxy(proxyTargetClass = true) 强制使用cglib
/**
* 这里指明了com.lupf.aop.service.ServiceB用于匹配代理后的对象
* <p>
* 为了方便测试 这里定义了一个接口I ServiceB实现了I
* <p>
* 第一次测试,
* 使用 @EnableAspectJAutoProxy 表示默认使用jdk动态代理
* 那么spring获取对象是 I serviceB = ac.getBean(ServiceB.class)
* 这样serviceB对象调用方法的时候,不会被增强
* 原因是因为jdk代理是基于接口,那么生成的代理对象如下
* public $ServiceBByJDK implements I {
* publis ServiceB serviceB;
* public void busi1(){
* service.busi();
* }
* <p>
* // other....
* }
* 所以$ServiceBByJDK无法匹配上ServiceB,增强失败
* <p>
* <p>
* ===============================================================
* <p>
* 第二次测试
*
* @EnableAspectJAutoProxy(proxyTargetClass = true) 强制使用cglib
* 那么就会得到一个基于cglib的代理实现
* 最终的代理对象效果如下
* public ServiceB$$EbhabcerBySpringCGLIB&&xxxx extend ServiceB implements I
* {
* // do something
* }
* 因为cglib代理是基于继承 因此生成的代理对象是ServiceB的子类,所以这里就可以匹配上
* 第二次测试执行的ServiceB中的所有方法都是可以增强的
*/
@Pointcut("this(com.lupf.aop.service.ServiceB)")
public void this_pc1() {
}
// 匹配动态代理之前 目标对象为指定对象的类
/**
* 由于是匹配的目标对象 因此就不会区分使用的什么代理方式了
* 因此jdk和cglib代理的对象都是可以增强的
*/
@Pointcut("target(com.lupf.aop.service.ServiceB)")
public void target_pc1() {
}
/**
* 类上面加了指定注解,那么该类下所有的方法执行都增强
* <p>
* 注意:该方式只作用于类上面 如果注解只写在方法上没加在类上面,那么增强是无法生效的
*/
@Pointcut("@within(com.lupf.aop.anno.Lupf)")
public void anno_within_pc() {
}
/**
* 代理对象的时候 目标对象添加了指定注解类的方法执行都会进行增强
*/
@Pointcut("@target(com.lupf.aop.anno.Lupf)")
public void anno_target_pc() {
}
/**
* 方法参数带有指定注解的
* <p>
* 如下测试,定义一个测试对象
*
* @Lupf public class BusiBean {
* }
* <p>
* 如下的方法是会进行增强的
* public String busi1(BusiBean busiBean)
* <p>
* 下面这种方式是不会进行增强的
* public void busi2(@Lupf String arg1)
*/
@Pointcut("@args(com.lupf.aop.anno.Lupf)")
public void anno_args_pc() {
}
/**
* 加了指定注解的方法调用的时候都会进行增强
* <p>
* 注意:这里只有方法,加在类上面是没有用的,和@within是对立的
*/
@Pointcut("@annotation(com.lupf.aop.anno.Lupf)")
public void anno_pc1() {
}
/**
* Before的执行时机是在方法执行之前执行
* 这里可以通过运算符去匹配多个切入点
*/
@Before("w_pc1() && w_pc2()")
public void advice1() {
System.out.println("aop before..........");
}
/**
* After执行时机是在方法执行完之后
* 这个执行并不会管执行成功还是失败,都会触发这个通知
*/
@After("anno_pc1()")
public void advice2() {
System.out.println("aop after.............");
}
/**
* 这个通知相对于After就更加明确了
* 只有在执行成功之后才会通知
* <p>
* 如果不需要管结果,那么我们只需要指定好切点就好了
* 如果我们需要拿到想要结果,就可以通过returning指定一个字段名称,并通过名称拿到响应数据
* 同时这里是可以指定具体的响应数据类型,当指定具体类型之后,就只有返回指定类型的数据才会触发通知
* <p>
* 这里是否需要返回 并不会影响到调用方
*
* @param re 响应数据
*/
// @AfterReturning("anno_pc1()")
@AfterReturning(pointcut = "anno_pc1()", returning = "re")
public void advice3(Object re) {
System.out.println("aop after reutrn........re:" + re);
}
/**
* 当方法执行出现异常之后,就会触发这个通知
* <p>
* 如果不需要管异常是什么,那么我们只需要指定好切点就好了
* 如果我们需要拿到异常,就可以通过throwing指定一个字段名称,并通过名称拿到相应的异常
* 同时这里是可以指定具体的异常类型,当指定具体类型之后,就只有抛出指定类型的异常才会触发通知
*
* @param ex
*/
//@AfterThrowing("anno_pc1()")
@AfterThrowing(pointcut = "anno_pc1()", throwing = "ex")
public void advice4(Exception ex) {
System.out.println("aop after exception msg:" + ex.getMessage());
}
/**
* 坏绕通知 囊括了上面4种通知时机
*
* @param joinPoint
* @return 响应数据
* @throws Throwable
*/
@Around("anno_pc1()")
public Object advice4(ProceedingJoinPoint joinPoint) throws Throwable {
//在这里相当于@Before
System.out.println("aop around before.........");
// 获取所有参数
Object[] args = joinPoint.getArgs();
try {
// 执行目标方法并获取响应对象
Object o = joinPoint.proceed(args);
// 到这里相当于@AfterReturning
System.out.println("aop around after returning.........");
// 返回数据
// 这里不同于@Before 这里如果不返回,调用方将拿不到响应数据
return o;
} catch (Throwable t) {
// 这里相当于@AfterThrowing
System.out.println("aop around after throwing.........");
throw t;
} finally {
// 到这里相当于@After 异常和正常只会出现其中一种情况
System.out.println("aop around after.........");
}
}
}
关于理解及使用就写到这里,以上纯属个人理解,如果存在不对的地方,欢迎留言指正!!!后续会一起来分析一下spring是如何去实现aop的;