
李沐泽
V1
2023/05/08阅读:18主题:默认主题
Springboot APO面向切面编程
Springboot APO面向切面编程
面向切面编程是对面向对象编程的补充
面向对象编程(OOP
)的好处是显而易见的额,缺点也同样明显。当需要为多个不具有集成关系的对象天剑一个公共方法的时候,例如日志记录、性能监控等,如果采用面向对象编程的方式,会产生较大的重复工作和大量的重复代码,不利于维护。面向切面编程(AOP
)是对面向对象编程的补充,简单来说就是统一处理某一"切面"的问题的编程思想,如果使用AOP
的方式进行日志记录和处理,所有的日志代码都集中在一处,不需要再每个方法里面都去添加,极大减少了重复代码。利用AOP
可以对我们边缘业务进行隔离,降低无关业务逻辑耦合性。提高程序的可重用性,同时提高了开发的效率。一般用于日志记录,性能统计,安全控制,权限管理,事务处理,异常处理,资源池管理
。
我们可以这样理解面向切面编程,在面向对象编程中,对象都是根据类来进行实例化的,类这个模具如果是生产面包,那么该类实例化的对象就是一个成型的面包,对类中成员变量进行赋值后,实例化的对象则无法改变,等着被使用,被回收等。面向切面编程,对于我们封装好的类,我们可以在编译期间或者运行期间,对其进行切割,在原有的方法里面织入一些新代码,对原有方法进行代码增强,即把面包切开,可以加入一个辅料,使得面包更美味。
基本概念
-
目标对象(Target)
指将要被增强的对象,即业务类对象,或者说是被切面所通知的对象。
-
切面(Aspect)
Aspect通常是一个类,里面定义了切入点和通知,它既包含了横切逻辑的定义,也包括了切入点的定义。Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。
可以简单地认为, 使用 @Aspect 注解的类就是切面/**
* 定义切面
*/
@Aspect
@Component
public class LogAspect {
} -
连接点(JoinPoint)
接点就是程序执行的某个特定的位置,如:类开始初始化前、类初始化后、类的某个方法调用前、类的某个方法调用后、方法抛出异常后等。因为Spring只支持方法类型的连接点,所以在Spring中连接点就是被拦截到的方法。
@Before("pointcut()")
public void log(JoinPoint joinPoint) { //这个JoinPoint参数就是连接点
} -
切入点(pointCut)
定义了在"什么地方"进行切入,即要对哪些类中的哪些方法进行增强,进行切割,指的是被增强的方法。即要切哪些东西。
Spring缺省使用AspectJ切入点语法。切入点就是提供一组规则(使用AspectJ pointcut expression language 来描述)来匹配连接点,给满足规则的连接点添加通知。/**
* 使用Pointcut给这个方法定义切点,即UserService中全部方法均为切点。
* 这里在这个log方法上面定义切点,然后就只需在下面的Before、After等等注解中填写这个切点方法"log()"即可设置好各个通知的切入位置。
* 其中:
* execution:代表方法被执行时触发
* *:代表任意返回值的方法
* com.liang.service.impl.UserServiceImpl:这个类的全限定名
* (..):表示任意的参数
*/
@Pointcut("execution(* com.liang.service.impl.UserServiceImpl.*(..))")
public void log(){
} -
通知(Advice)
指拦截到连接点之后要执行的代码,包括"around"、“”、“”、“”、等不同类型的通知。
/**
* 异常通知
*/
@AfterThrowing("log()")
public void doThrowing() {
logger.error("方法抛出异常!");
} -
织入(Weaving)
就是把切面加入到核心业务逻辑的过程,织入可以在编译期织入,类装载期织入,动态代理织入。Spring采用的是动态代理织入,而AspectJ采用编译期织入和类装载期织入。
Spring Boot AOP开发
-
添加依赖
<!-- 在Spring Boot中,我们使用@AspectJ注解开发AOP,首先需要在pom.xml中引入依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
-
前期准备(业务逻辑代码编写)
/**
* User实体类
*/
public class User {
private Long id;
private String username;
private String nikeName;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getNikeName() {
return nikeName;
}
public void setNikeName(String nikeName) {
this.nikeName = nikeName;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", nikeName='" + nikeName + '\'' +
'}';
}
}public interface UserService {
/**
* 打印用户信息
* @param user
*/
public void printUser(User user);
}@Service
public class UserServiceImpl implements UserService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void printUser(User user) {
logger.info("用户信息:" + user.toString());
}
}@RestController
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/printUser")
public String printUser(@RequestBody User user) {
userService.printUser(user);
return "用户信息打印完成!";
}
}image-20230508135918139 -
定义切面
/**
* 定义切面
*/
@Aspect
@Component
public class LogAspect {
/**
* 日志打印
*/
private Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 使用Pointcut定义切点
* 其中:
* execution:代表方法被执行时触发
* *:代表任意返回值的方法
* com.liang.service.impl.UserServiceImpl:这个类的全限定名
* (..):表示任意的参数
*/
@Pointcut("execution(* com.liang.service.impl.UserServiceImpl.*(..))")
public void log(){
}
/**
* 前置通知
*/
@Before("log()")
public void doBefore() {
logger.warn("调用方法之前");
}
/**
* 后置通知
*/
@After("log()")
public void doAfter() {
logger.warn("调用方法之后");
}
/**
* 返回通知
*/
@AfterReturning("log()")
public void doReturning() {
logger.warn("方法正常返回之后");
}
/**
* 异常通知
*/
@AfterThrowing("log()")
public void doThrowing() {
logger.error("方法抛出异常!");
}image-20230508140102072 -
环绕通知
/**
* 环绕通知是AOP中最强大的通知,可以同时实现前置和后置通知,
* 不过它的可控性没那么强,如果不用大量改变业务逻辑,一般不需要用到它。
* 通知方法中有一个ProceedingJoinPoint类型参数,
* 通过其proceed方法来调用原方法。需要注意的是环绕通知是会覆盖原方法逻辑的,
* 如果上面代码不执行joinPoint.proceed();这一句,就不会执行原被织入方法。
* 因此环绕通知一定要调用参数的proceed方法,这是通过反射实现对被织入方法调用。
* @param joinPoint
*/
@Around("log()")
public void around(ProceedingJoinPoint joinPoint){
logger.warn("执行环绕通知之前:");
try {
joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
logger.warn("执行环绕通知之后");
}image-20230508140349283
-
通知方法传参
/**
* 在注解后面加一个args选项,里面写参数名即可。
* 需要注意的是,通知方法的参数必须和被织入方法参数一一对应
*/
@Before("log() && args(user)")
public void doBefore(User user) {
logger.warn("调用方法之前 " + user);
}image-20230508140745572 -
连接点作为参数传入
/**
* 连接点作为参数传入
* 获得类名,方法名,参数等信息
*/
@Before("log()")
public void doBefore(JoinPoint joinPoint) {
System.out.println(joinPoint.getClass().getName());
System.out.println(joinPoint.getSignature());
System.out.println(joinPoint.getSignature().getName());
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
System.out.println(arg);
}
logger.warn("调用方法之前 " );
}SpringBoot实现对自定义注解的切面
-
自定义一个注解作为切点
/**
* 自定义注解 检查权限
*/
@Target(ElementType.METHOD) //该注解作用在方法上
@Retention(RetentionPolicy.RUNTIME) //该注解的作用范围,运行时能够识别该注解
public @interface CheckOperateAuth {
String value();
} -
在切面类中去编写鉴权的逻辑
/**
* 切面
*/
@Aspect
@Component
public class AuthAspect {
/**
* @Before注解用于标注一个方法,在被这个注解的方法执行之前会被执行,它可以用来初始化所需要的资源,实例化对象或者准备测试数据。
* 这里的value属性表示注解所需要过滤的目标注解。在这个例子中,这个方法将会在任何一个被@checkOperateAuth注解的方法执行之前执行。
* @param joinPoint
* @param checkOperateAuth
*/
@Before(value = "@annotation(checkOperateAuth)" )
public void before(JoinPoint joinPoint, CheckOperateAuth checkOperateAuth){
if (!hasAnnotation(joinPoint)){
throw new RuntimeException("您没有该权限");
}
}
/**
* 先判断该方法或者类上面是否具有checkOperateAuth注解
* 如果有则判断是否有权限
* @param joinPoint
* @return
*/
public boolean hasAnnotation(JoinPoint joinPoint){
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//获取切点上的方法
Method method = methodSignature.getMethod();
// 获取方法上的该注解
CheckOperateAuth annotation = method.getAnnotation(CheckOperateAuth.class);
//方法上没有获取类上的
if (annotation == null){
annotation = method.getDeclaringClass().getAnnotation(CheckOperateAuth.class);
}
//如果都没有就不进行鉴权
if (annotation == null){
return true;
}
//有该注解并且有值,就进行鉴权操作
if (StringUtils.hasLength(annotation.value())){
//模拟获取当前登录用户所拥有的权限
Set<String> permissionSet = new HashSet<>();
permissionSet.add("sys.user.list");
permissionSet.add("sys.user.add");
if (CollectionUtils.isEmpty(permissionSet)){
return false;
}
return permissionSet.contains(annotation.value());
}
return true;
}
} -
在需要鉴权的地方加上自定义注解
@GetMapping("list")
@CheckOperateAuth("sys.user.list")
public String list(){
return "访问成功";
}
@PostMapping("delete")
@CheckOperateAuth("sys.user.delete")
public String delete(){
return "访问成功";
}
-
作者介绍

李沐泽
V1