什么是AOP
AOP:Aspect Oriented Programming,中文翻译为”面向切面编程“。面向切面编程是一种编程范式,它作为OOP面向对象编程的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理、权限控制、缓存控制、日志打印等等。AOP采取横向抽取机制,取代了传统纵向继承体系的重复性代码
AOP把软件的功能模块分为两个部分:核心关注点和横切关注点。业务处理的主要功能为核心关注点,而非核心、需要拓展的功能为横切关注点。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点进行分离
使用AOP有诸多好处,如:
1.集中处理某一关注点/横切逻辑
2.可以很方便的添加/删除关注点
3.侵入性少,增强代码可读性及可维护性
AOP的术语
1.Join point(连接点)
Spring 官方文档的描述:
程序执行过程中的一个点,如方法的执行或异常的处理。在Spring AOP中,连接点总是表示方法的执行。通俗的讲,连接点即表示类里面可以被增强的方法
2.Pointcut(切入点)
切入点是与连接点匹配的表达式,用于确定是否需要执行通知。切入点使用与连接点匹配的不同类型的表达式,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中的内容无论切入点是否抛出异常,都将执行