Spring AOP用法详解
什么是AOP
AOP:Aspect Oriented Programming,中文翻译为”面向切面编程“。面向切面编程是一种编程范式,它作为OOP面向对象编程的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理、权限控制、缓存控制、日志打印等等。AOP采取横向抽取机制,取代了传统纵向继承体系的重复性代码
AOP把软件的功能模块分为两个部分:核心关注点和横切关注点。业务处理的主要功能为核心关注点,而非核心、需要拓展的功能为横切关注点。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点进行分离
使用AOP有诸多好处,如:
1.集中处理某一关注点/横切逻辑
2.可以很方便的添加/删除关注点
3.侵入性少,增强代码可读性及可维护性
AOP的术语
1.Join point(连接点)
Spring 官方文档的描述:
A point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.
程序执行过程中的一个点,如方法的执行或异常的处理。在Spring AOP中,连接点总是表示方法的执行。通俗的讲,连接点即表示类里面可以被增强的方法
2.Pointcut(切入点)
Pointcut are expressions that is matched with join points to determine whether advice needs to be executed or not. Pointcut uses different kinds of expressions that are matched with the join points and Spring framework uses the AspectJ pointcut expression language
切入点是与连接点匹配的表达式,用于确定是否需要执行通知。切入点使用与连接点匹配的不同类型的表达式,Spring框架使用AspectJ切入点表达式语言。我们可以将切入点理解为需要被拦截的Join point
3.Advice(增强/通知)
所谓通知是指拦截到Joinpoint之后所要做的事情就是通知,通知分为前置通知、后置通知、异常通知、最终通知和环绕通知(切面要完成的功能)
4.Aspect(切面)
Aspect切面表示Pointcut(切入点)和Advice(增强/通知)的结合
Spring AOP用法
示例代码
/**
* 设置登录用户名
*/
public class CurrentUserHolder {
private static final ThreadLocal<String> holder = new ThreadLocal<>();
public static String get() {
return holder.get();
}
public static void set(String user) {
holder.set(user);
}
}
/**
* 校验用户权限
*/
@Service("authService")
public class AuthServiceImpl implements AuthService {
@Override
public void checkAccess() {
String user = CurrentUserHolder.get();
if(!"admin".equals(user)) {
throw new RuntimeException("该用户无此权限!");
}
}
}
/**
* 业务逻辑类
*/
@Service("productService")
public class ProductServiceImpl implements ProductService {
@Autowired
private AuthService authService;
@Override
public Long deleteProductById(Long id) {
System.out.println("删除商品id为" + id + "的商品成功!");
return id;
}
@Override
public void deleteProductByName(String name) {
System.out.println("删除商品名称为" + name + "的商品成功!");
}
@Override
public void selectProduct(Long id) {
if("100".equals(id.toString())) {
System.out.println("查询商品成功!");
} else {
System.out.println("查询商品失败!");
throw new RuntimeException("该商品不存在!");
}
}
}
1.使用within表达式匹配包类型
//匹配ProductServiceImpl类里面的所有方法
@Pointcut("within(com.aop.service.impl.ProductServiceImpl)")
public void matchType() {}
//匹配com.aop.service包及其子包下所有类的方法
@Pointcut("within(com.aop.service..*)")
public void matchPackage() {}
2.使用this、target、bean表达式匹配对象类型
//匹配AOP对象的目标对象为指定类型的方法,即ProductServiceImpl的aop代理对象的方法
@Pointcut("this(com.aop.service.impl.ProductServiceImpl)")
public void matchThis() {}
//匹配实现ProductService接口的目标对象
@Pointcut("target(com.aop.service.ProductService)")
public void matchTarget() {}
//匹配所有以Service结尾的bean里面的方法
@Pointcut("bean(*Service)")
public void matchBean() {}
3.使用args表达式匹配参数
//匹配第一个参数为Long类型的方法
@Pointcut("args(Long, ..) ")
public void matchArgs() {}
4.使用@annotation、@within、@target、@args匹配注解
//匹配标注有AdminOnly注解的方法
@Pointcut("@annotation(com.aop.annotation.AdminOnly)")
public void matchAnno() {}
//匹配标注有Beta的类底下的方法,要求annotation的Retention级别为CLASS
@Pointcut("@within(com.google.common.annotations.Beta)")
public void matchWithin() {}
//匹配标注有Repository的类底下的方法,要求annotation的Retention级别为RUNTIME
@Pointcut("@target(org.springframework.stereotype.Repository)")
public void matchTarget() {}
//匹配传入的参数类标注有Repository注解的方法
@Pointcut("@args(org.springframework.stereotype.Repository)")
public void matchArgs() {}
5.使用execution表达式
execution表达式是我们在开发过程中最常用的,它的语法如下:
modifier-pattern:用于匹配public、private等访问修饰符
ret-type-pattern:用于匹配返回值类型,不可省略
declaring-type-pattern:用于匹配包类型
modifier-pattern(param-pattern):用于匹配类中的方法,不可省略
throws-pattern:用于匹配抛出异常的方法
代码示例:
@Component
@Aspect
public class SecurityAspect {
@Autowired
private AuthService authService;
//匹配com.aop.service.impl.ProductServiceImpl类下的方法名以delete开头、参数类型为Long的public方法
@Pointcut("execution(public * com.aop.service.impl.ProductServiceImpl.delete*(Long))")
public void matchCondition() {}
//使用matchCondition这个切入点进行增强
@Before("matchCondition()")
public void before() {
System.out.println("before 前置通知......");
authService.checkAccess();
}
}
单元测试:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootApplicationTests {
@Autowired
private ProductService productService;
@Test
public void contextLoads() {
//设置用户名
CurrentUserHolder.set("hello");
productService.selectProduct(100L);
productService.deleteProductByName("衣服");
productService.deleteProductById(100L);
}
}
运行结果(只有deleteProductById方法拦截成功):
查询商品成功!
删除商品名称为衣服的商品成功!
before 前置通知......
java.lang.RuntimeException: 该用户无此权限!
at com.aop.service.impl.AuthServiceImpl.checkAccess(AuthServiceImpl.java:15)
at com.aop.security.SecurityAspect.before(SecurityAspect.java:50)
可以在多个表达式之间使用连接符匹配多个条件, 如使用||表示“或”,使用 &&表示“且”
//匹配com.aop.service.impl.ProductServiceImpl类下方法名以select或delete开头的所有方法
@Pointcut("execution(* com.aop.service.impl.ProductServiceImpl.select*(..)) || " +
"execution(* com.aop.service.impl.ProductServiceImpl.delete*(..))")
public void matchCondition() {}
//使用matchCondition这个切入点进行增强
@Before("matchCondition()")
public void before() {
System.out.println("before 前置通知......");
authService.checkAccess();
}
单元测试:
@Test
public void contextLoads() {
CurrentUserHolder.set("admin");
productService.selectProduct(100L);
productService.deleteProductByName("衣服");
productService.deleteProductById(100L);
}
运行结果(所有方法均拦截成功):
before 前置通知......
查询商品成功!
before 前置通知......
删除商品名称为衣服的商品成功!
before 前置通知......
删除商品id为100的商品成功!
6.Advice注解
Advice注解一共有五种,分别是:
1.@Before前置通知
前置通知在切入点运行前执行,不会影响切入点的逻辑
2.@After后置通知
后置通知在切入点正常运行结束后执行,如果切入点抛出异常,则在抛出异常前执行
3.@AfterThrowing异常通知
异常通知在切入点抛出异常前执行,如果切入点正常运行(未抛出异常),则不执行
4.@AfterReturning返回通知
返回通知在切入点正常运行结束后执行,如果切入点抛出异常,则不执行
5.@Around环绕通知
环绕通知是功能最强大的通知,可以在切入点执行前后自定义一些操作。环绕通知需要负责决定是继续处理join point(调用ProceedingJoinPoint的proceed方法)还是中断执行
示例代码:
//匹配com.aop.service.impl.ProductServiceImpl类下面的所有方法
@Pointcut("execution(* com.aop.service.impl.ProductServiceImpl.*(..))")
public void matchAll() {}
@Around("matchAll()")
public Object around(ProceedingJoinPoint joinPoint) {
Object result = null;
authService.checkAccess();
System.out.println("befor 在切入点执行前运行");
try{
result = joinPoint.proceed(joinPoint.getArgs());//获取参数
System.out.println("after 在切入点执行后运行,result = " + result);
} catch (Throwable e) {
System.out.println("after 在切入点执行后抛出exception运行");
e.printStackTrace();
} finally {
System.out.println("finally......");
}
return result;
}
单元测试:
@Test
public void contextLoads() {
CurrentUserHolder.set("admin");
productService.deleteProductById(100L);
productService.selectProduct(10L);
}
运行结果:
before 在切入点执行前运行
删除商品id为100的商品成功!
after 在切入点执行后运行,result = 100
finally......
before 在切入点执行前运行
查询商品失败!
after 在切入点执行后抛出exception运行
java.lang.RuntimeException: 该商品不存在!
at com.aop.service.impl.ProductServiceImpl.selectProduct(ProductServiceImpl.java:41)
at com.aop.service.impl.ProductServiceImpl$$FastClassBySpringCGLIB$$f17a76a2.invoke(<generated>)
finally......
在执行ProceedingJoinPoint对象的proceed方法前相当于Before前置通知;执行proceed方法相当于运行切入点(同时可以获取参数);在方法执行之后相当于After后置通知,如果运行切入点抛出异常,则catch中的内容相当于AfterThrowing异常通知;finally中的内容无论切入点是否抛出异常,都将执行