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 "用户信息打印完成!"; } }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nKpoIF0b-1683531407468)(C:\Users\liang\AppData\Roaming\Typora\typora-user-images\image-20230508135918139.png)]
-
定义切面
/** * 定义切面 */ @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("方法抛出异常!"); }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6zIjcgq3-1683531407469)(C:\Users\liang\AppData\Roaming\Typora\typora-user-images\image-20230508140102072.png)]
-
环绕通知
/** * 环绕通知是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("执行环绕通知之后"); }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pmePoVht-1683531407469)(C:\Users\liang\AppData\Roaming\Typora\typora-user-images\image-20230508140349283.png)]
-
通知方法传参
/** * 在注解后面加一个args选项,里面写参数名即可。 * 需要注意的是,通知方法的参数必须和被织入方法参数一一对应 */ @Before("log() && args(user)") public void doBefore(User user) { logger.warn("调用方法之前 " + user); }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vP75Yc32-1683531407469)(C:\Users\liang\AppData\Roaming\Typora\typora-user-images\image-20230508140745572.png)]
-
连接点作为参数传入
/** * 连接点作为参数传入 * 获得类名,方法名,参数等信息 */ @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 "访问成功"; }
-