目录
3、定义切面,通过环绕通知增强方法-ProceedingJoinPoint
一、AOP面向切面编程
AOP 的全称是“Aspect Oriented Programming”,即面向切面编程,和 OOP(面向对象编程)类似,也是一种编程思想。
AOP 采取横向抽取机制(动态代理),取代了传统纵向继承机制的重复性代码,其应用主要体现在事务处理、日志管理、权限控制、异常处理等方面。主要作用是分离功能性需求和非功能性需求,使开发人员可以集中处理某一个关注点或者横切逻辑,减少对业务代码的侵入,增强代码的可读性和可维护性。
简单的说,AOP 的作用就是保证开发者在不修改源代码的前提下,为系统中的业务组件添加某种通用功能。AOP 就是代理模式的典型应用。
目前最流行的 AOP 框架有两个,分别为 Spring AOP 和 AspectJ。
Spring AOP 是基于 AOP 编程模式的一个框架,它能够有效的减少系统间的重复代码,达到松耦合的目的。Spring AOP 使用纯 Java 实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。有两种实现方式:基于接口的JDK动态代理和基于继承的CGLIB动态代理。
AspectJ 是一个基于 Java 语言的 AOP 框架,从 Spring 2.0 开始,Spring AOP 引入了对 AspectJ 的支持。AspectJ 扩展了 Java 语言,提供了一个专门的编译器,在编译时提供横向代码的植入。
图解AOP思想,底层使用动态代理来实现原有代码的增强。
如果想了解动态代理,可以阅读我的这篇文章
二、Spring-AOP的简单实现
本文使用Springboot(偷懒,不想引太多jar包,哈哈),首先Springboot实现切面编程,需要引入相关依赖
<!--切面AOP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
1、定义被代理类的接口和实现类
如果被代理类有接口,AOP底层默认使用JDK动代理,否则使用Cglib动态代理。
声明接口和接口实现类
// 声明接口
public interface AopService {
void queryById(String userId);
void deleteById(String userId);
}
// 接口实现类
@Service
public class AopServiceImpl implements AopService {
@Override
public void queryById(String id) {
System.out.println("[AOP测试]:queryById-查询执行,参数:" + id);
}
@Override
public void deleteById(String id) {
System.out.println("[AOP测试]:deleteById-删除执行,参数:" + id);
}
}
2、定义切面:@Aspect+@Component
定义切面,非常简单,主要是使用AspectJ相关注解,然后把定义的切面注入到IOC容器中。
@Component
@Aspect
public class Logging { // 切面类
/**
* 前置通知
* execution表达式,指定被代理的目标对象。
*/
@Before("execution(* com.swadian.spring.aop.aopservice.impl..*.*(..))") //通知 + 目标对象
public void beforeAdvice(){ // 连接点(方法)->模拟日志输出
System.out.println("[logging]:日志前置通知.");
}
/**
* 后置通知
*/
@After("execution(* com.swadian.spring.aop.aopservice.impl..*.*(..))")
public void afterAdvice(){ // 连接点(方法)->模拟日志输出
System.out.println("[logging]:日志后置通知.");
}
}
注:execution()支持匹配多个,如 : "execution(* com.ws..*.*(*)) || execution(* com.db..*.*(*))";
测试切面
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class UserServiceImplTest {
@Resource
private AopService aopService;
@Test
public void findUserById() {
aopService.queryById("888");
}
@Test
public void deleteById() {
aopService.deleteById("999");
}
}
// 测试结果
[logging]:日志前置通知.
[AOP测试]:queryById-查询执行,参数:888
[logging]:日志后置通知.
------------------------------------
[logging]:日志前置通知.
[AOP测试]:deleteById-删除执行,参数:999
[logging]:日志后置通知.
如果不会写单元测试,请阅读我的这篇文章
引入切点
上边定义的切面中,对相同的execution表达式重复写了多次,为了避免这种重复代码,可以为多个连接点定义一个统一的切点,减少重复代码。改造后的代码如下
@Component
@Aspect
public class Logging {
/**
* 申明一个切点
*/
@Pointcut("execution(* com.swadian.spring.aop.aopservice.impl..*.*(..))")
private void aopPointTest(){}
/**
* 前置通知
*/
@Before("aopPointTest()")
public void beforeAdvice(){
System.out.println("[logging]:日志前置通知.");
}
/**
* 后置通知
*/
@After("aopPointTest()")
public void afterAdvice(){
System.out.println("[logging]:日志后置通知.");
}
}
三、使用AOP环绕通知和注解实现权限验证
1、自定义需要被解析的注解
AOP和注解一起使用,通常可以非常好的简化我们的代码。比如在日志记录或者权限验证等场景中就可以使用到
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginAnnotation { //自定义注解
// 登录名
String loginName() default "游客";
// 角色
String role() default "";
}
2、申明接口和接口的实现类
// 接口(非必须) ——> 使用接口,底层会进行JDK动态代理
public interface LoginService {
public void login();
}
// 实现类
@Service
public class LoginServiceImpl implements LoginService {
@LoginAnnotation(loginName = "sam", role = "MANAGE")
public void login(){ // 需要进行权限验证的方法
System.out.println("用户认证后,方法执行...");
}
}
spring Aop 底层用了动态代理还是 cglib?
- 如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制);
- 如果要被代理的对象不是个实现类,那么,Spring会强制使用CGLib来实现动态代理。
3、定义切面,通过环绕通知增强方法-ProceedingJoinPoint
AOP环绕通知与前置、后置通知的区别在于,环绕通知需要手动调用目标方法,需要指定在什么情况下或者在什么地方调用。
import com.swadian.spring.aop.aopservice.impl.LoginAnnotation;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Component
@Aspect
public class LoginAspect {
/**
* 申明一个切点
*/
@Pointcut("execution(* com.swadian.spring.aop.aopservice.impl..*.*(..))")
private void aopPointTest() {
}
/**
* 环绕通知
*/
@Around("aopPointTest()")
public void recordLoginInfo(ProceedingJoinPoint pjp) throws Throwable {
// 首先获取ProceedingJoinPoint 签名 (方法有签名 方法名称 返回值类型 参数类型 及 参数个数)
Signature signature = pjp.getSignature();
if (signature instanceof MethodSignature) {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
// 判断方法上的注解是不是权限验证注解
if (method.isAnnotationPresent(LoginAnnotation.class)) {
LoginAnnotation annotation = method.getAnnotation(LoginAnnotation.class);
if (annotation.role().equals("MANAGE")) {
System.out.println("[AOP权限验证]:权限验证通过");
pjp.proceed(); // 调用目标方法
System.out.println("[AOP权限验证]:被代理方法执行结束");
} else {
System.out.println("[AOP权限验证]:该用户没有访问权限!");
}
}
}
}
}
可以看到在上面的代码中,定义通知的时候在通知方法中添加了入参:ProceedingJoinPoint。在创建环绕通知的时候,这个参数是必须写的。因为在需要在通知中使用ProceedingJoinPoint.proceed()方法调用被通知的方法。
另外,如果忘记调用proceed()方法,那么被代理方法将不会执行。
获取当前执行方法所属的包名、类名:
Object target = pjp.getTarget();
String className = target.getClass().getName(); //当前执行方法所属的类、包
测试:
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class LoginServiceImplTest {
@Autowired
LoginService loginService;
@Test
public void login() {
loginService.login();
}
}
// 测试结果
[AOP权限验证]:权限验证通过
用户认证后,方法执行...
[AOP权限验证]:被代理方法执行结束
四、execution()语法定义
例:定义切入点表达式 execution(* com.sample.service.impl..*.*(..))
execution()是最常用的切点函数,其语法如下所示:
整个表达式可以分为五个部分:
- execution(): 表达式主体。
- 第一个*号:表示返回类型,*号表示所有的类型。
- 包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.sample.service.impl包、子孙包下所有类的方法。
- 第二个*号:表示类名,*号表示所有的类。
- *(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。