AOP-Demo
一、概念
AOP:面向切面编程,是对面向对象编程的一种补充。
常见的业务场景:统一日志处理
二、背景
现在我有一段代码如下:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>aop-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.boot.version>2.2.2.RELEASE</spring.boot.version>
</properties>
<!--环境: SpringBoot + Spring MVC + Spring AOP-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!--基于 Spring Boot 的 Spring MVC-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
</dependencies>
</project>
CalculatorController.java
package com.example.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* Created by 19921224 on 2023/8/15 14:59
*/
@RestController
@RequestMapping("/cal")
public class CalculatorController {
@GetMapping("/add/{a}/{b}")
public Map<String, Object> add(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
System.out.println(a + " + " + b + " = " + (a + b));
Map<String, Object> map = new HashMap<>();
map.put("code", "000000");
map.put("msg", "成功");
map.put("data", a + b);
return map;
}
@GetMapping("/sub/{a}/{b}")
public Map<String, Object> sub(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
System.out.println(a + " - " + b + " = " + (a - b));
Map<String, Object> map = new HashMap<>();
map.put("code", "000000");
map.put("msg", "成功");
map.put("data", a - b);
return map;
}
@GetMapping("/multi/{a}/{b}")
public Map<String, Object> multi(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
System.out.println(a + " * " + b + " = " + (a * b));
Map<String, Object> map = new HashMap<>();
map.put("code", "000000");
map.put("msg", "成功");
map.put("data", a * b);
return map;
}
@GetMapping("/div/{a}/{b}")
public Map<String, Object> div(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
System.out.println(a + " / " + b + " = " + (a / b));
Map<String, Object> map = new HashMap<>();
map.put("code", "000000");
map.put("msg", "成功");
map.put("data", a / b);
return map;
}
}
现在我们访问看一下接口是否正常:
问题描述
可以看到,我在上面的4个方法中都添加了打印输出日志的功能,这样实在是比较麻烦,尤其当方法比较多以后,那该如何解决类似的日志输出问题呢?
三、使用 Spring AOP 改造
其原理如下:
对于上面的四个方法进行横切,每个方法横切后都会出现一个如下所示的切面,然后我们对这4个切面进行抽象为切面对象,即为面向切面编程。之后只需要把想实现的日志功能添加在这个抽象出来的切面对象中即可。
改造步骤
步骤一:自定义注解
我们需要让AOP知道目标方法在哪里(即AOP需要处理的是哪些方法),那怎么让它知道呢?这里我们采用通过
注解
的方式,因此需要自定义注解
package com.example.annotation;
import java.lang.annotation.*;
/**
* Created by 19921224 on 2023/8/15 15:53
* 自定义日志注解,至少需要如下 三个元注解(元注解:描述注解的注解)
*/
@Target(ElementType.METHOD) // 该注解作用的范围
@Retention(RetentionPolicy.RUNTIME) // 该注解运行的机制
@Documented
public @interface LogAnnotation {
String value() default ""; // 这是一个方法,到时候就可以通过 @LogAnnotation("作用") 来使用了
}
步骤二:使用注解进行标记
将上面的自定义注解作用在你想要添加日志的方法上即可,改造后代码如下:
package com.example.controller;
import com.example.annotation.LogAnnotation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* Created by 19921224 on 2023/8/15 14:59
*/
@RestController
@RequestMapping("/cal")
public class CalculatorController {
@LogAnnotation("加法")
@GetMapping("/add/{a}/{b}")
public Map<String, Object> add(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
// System.out.println(a + " + " + b + " = " + (a + b));
Map<String, Object> map = new HashMap<>();
map.put("code", "000000");
map.put("msg", "成功");
map.put("data", a + b);
return map;
}
@LogAnnotation("减法")
@GetMapping("/sub/{a}/{b}")
public Map<String, Object> sub(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
// System.out.println(a + " - " + b + " = " + (a - b));
Map<String, Object> map = new HashMap<>();
map.put("code", "000000");
map.put("msg", "成功");
map.put("data", a - b);
return map;
}
@LogAnnotation("乘法")
@GetMapping("/multi/{a}/{b}")
public Map<String, Object> multi(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
// System.out.println(a + " * " + b + " = " + (a * b));
Map<String, Object> map = new HashMap<>();
map.put("code", "000000");
map.put("msg", "成功");
map.put("data", a * b);
return map;
}
@LogAnnotation("除法")
@GetMapping("/div/{a}/{b}")
public Map<String, Object> div(@PathVariable("a") Integer a, @PathVariable("b") Integer b) {
// System.out.println(a + " / " + b + " = " + (a / b));
Map<String, Object> map = new HashMap<>();
map.put("code", "000000");
map.put("msg", "成功");
map.put("data", a / b);
return map;
}
}
步骤三:实现切面的业务逻辑
即将日志功能统一添加到抽象出来的切面中,而这个切面是一个
对象
(它是从一系列的切面中抽象出来的对象),那么我们要想对一个对象
进行的相关的操作,是不是需要一个类
来操作呢?因此,我们需要生成一个切面类
(从而由这个切面类
来生成 上图 3-1 中的切面对象
),如下:
切面对象
package com.example.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* Created by 19921224 on 2023/8/15 16:11
* 切面类
*/
@Component // 由于是Spring Boot项目,因此已经使用到了Spring IoC,所以我们只需要向Spring容器表明它是容器中的一个组件即可,就会自动生成对象
// 同时需要注意:启动类中的配置扫描可以扫描到该类,这样才会自动注入进去IoC中
@Aspect // 另外这个对象,它不是一个普通的对象,它是一个特殊的对象,叫切面对象,因此需要用 @Aspect 注解单独来标志它是一个切面对象
public class LogAspect {
// 将刚才 @LogAnnotation 注解标注的方法与 切面对象建立联系
@Pointcut("@annotation(com.example.annotation.LogAnnotation)") // 即 @LogAnnotation 注解标注的地方即为 切点
public void logPointCut() {
// 该方法仅仅是找出 切点的
}
// 真正实现的业务逻辑(此处是 日志打印功能)
@Around("logPointCut()") // 将切点与 around 联系起来
public Object around(ProceedingJoinPoint point) throws Throwable { // 不同的方法,传入的连接点对象不同
// 日志输出
String methodName = point.getSignature().getName();
// 通过反射 获取 @LogAnnotation 注解的 value 描述
//// 1. 先获取 切点 注解的 目标方法签名
MethodSignature signature = (MethodSignature) point.getSignature();
//// 2. 获取方法
Method method = signature.getMethod();
//// 3. 获取方法上的注解
LogAnnotation annotation = method.getAnnotation(LogAnnotation.class);
//// 如果 annotation 为 null,说明该方法上没有使用 @LogAnnotation 注解
if (annotation != null) {
//// 4. 获取注解的value值
String value = annotation.value();
System.out.println("【系统日志】 当前操作: " + value + "调用了 " + methodName + " 方法,返回值是: " + point.proceed());
}
// 调用连接点方法的返回值
return point.proceed();
}
}
然后这时候启动程序,请求接口,打印如下:
针对任意类的异常拦截处理
如果不仅仅是想只针对Controller层的异常进行拦截,需要可以拦截任意类的异常,采用下面的实现方式:
// 自定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CustomExceptionHandler {}
// AOP 切面
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class CustomExceptionHandlerAspect {
@Pointcut("@within(com.yourpackage.CustomExceptionHandler)")
public void customExceptionHandlerPointcut() {}
@Around("customExceptionHandlerPointcut()")
public Object handleException(ProceedingJoinPoint joinPoint) throws Throwable {
try {
return joinPoint.proceed();
} catch (Exception e) {
// 在这里处理异常并自定义返回值
return handleCustomException(e);
}
}
private Object handleCustomException(Exception e) {
// 根据异常类型自定义返回值
if (e instanceof IllegalArgumentException) {
return new CustomResponse("Invalid argument", 400);
} else if (e instanceof RuntimeException) {
return new CustomResponse("Internal server error", 500);
}
// 可以根据需要添加更多的异常处理逻辑
return new CustomResponse("Unexpected error", 500);
}
}
// 自定义响应对象
public class CustomResponse {
private String message;
private int statusCode;
public CustomResponse(String message, int statusCode) {
this.message = message;
this.statusCode = statusCode;
}
// Getters and setters
}
// 使用示例
@CustomExceptionHandler
@Service
public class YourService {
public void someMethod() {
// 你的业务逻辑
throw new IllegalArgumentException("Some error occurred");
}
}
本文来自博客园,作者:LoremMoon,转载请注明原文链接:https://www.cnblogs.com/hello-cnblogs/p/17632604.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南