目录
一、AOP面向切面编程
二、Spring-AOP的简单实现
1、定义被代理类的接口和实现类
2、定义一个切面:@Aspect+@Component
三、注解+切面环绕通知的使用
1、申明一个注解
2、申明接口和接口的实现类
3、设置切面,使用环绕通知-ProceedingJoinPoint
四、execution()语法定义
一、AOP面向切面编程
OOP(Object Oriented Programming ) 面向对象编程,万物皆对象!
AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。
不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。
日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

?AOP切面编程:在不改变原程序的基础上为代码段增加新的功能
图解AOP思想
底层使用动态代理来实现原有代码的增强。

如果想了解动态代理,可以阅读我的这篇文章
静态代理、动态代理和GCLib代理
二、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
模拟日志的前后输入和输出
@Component
@Aspect
public class Logging {
/**
* 前置通知
*/
@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]:日志后置通知.");
}
}
代码中@Before()是前置通知,@After()是后置通知。还有环绕通知,后边再做介绍。
代码中可以看到,切面的定义很简单。首先定义通知方法,然后通过execution表达式,指明需要在具体哪些方法前/后使用该通知。
测试
@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]:日志后置通知.
如果不会写单元测试,请阅读我的这篇文章
编写简单的SpringBoot单元测试类
改进——引入切点
上边切面的定义中,我们对相同的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]:日志后置通知.");
}
}
三、注解+切面环绕通知的使用
1、申明一个注解
切面常常配合注解一起使用,可以用来做日志记录或者权限验证
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、申明接口和接口的实现类
本实验中为了方便测试,所以都申明了接口,实际情况中,注解会放在Controller层,Controller层是没有接口的,此时AOP底层使用的是CGlib动态代理
// 接口
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
package com.swadian.spring.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()方法,那么被代理方法将不会执行。
测试:
@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包、子孙包下所有类的方法。
- ?第二个*号:表示类名,*号表示所有的类。
- ?*(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。
注:表达式支持匹配多个
如 : "execution(* com.ws..*.*(*)) || execution(* com.db..*.*(*))";
|