AOP(Aspect-Oriented Programming)是一种编程范式,它允许开发者在不修改源代码的情况下,对代码进行横切关注点的分离和增强。在 Java 中,AOP 通常通过使用 Spring Framework 或 AspectJ 等框架来实现。
在这篇博客中,我们将介绍 AOP 的基本概念、使用场景以及如何在 Java 中使用 Spring Framework 实现 AOP。
目录
一、前言
二、AOP 的基本概念
三、AOP 的使用场景
四、实现 AOP
4.1新建注解@Log
4.2新建LogAspect类
4.1.1依赖
4.1.2表结构
4.1.3实体类
4.1.4说明
4.1.5怎么调用
4.1.6版本号
五、总结
一、前言
AOP(Aspect-Oriented Programming)是一种编程范式,它允许开发者在不修改源代码的情况下,对代码进行横切关注点的分离和增强。在 Java 中,AOP 通常通过使用 Spring Framework 或 AspectJ 等框架来实现。
二、AOP 的基本概念
AOP 是一种面向切面编程的思想,它将程序中的业务逻辑和系统级服务(如日志记录、事务管理、权限控制等)分离开来,使得系统级服务可以在不修改业务逻辑代码的情况下进行添加、修改或删除。
AOP 中的核心概念包括:
- 切面(Aspect):切面是一个关注点的集合,它定义了在哪些地方需要进行增强。
- 连接点(JoinPoint):连接点是程序执行过程中的一个点,如方法调用、异常抛出等。
- 通知(Advice):通知是在连接点处执行的操作,它可以是前置通知、后置通知、异常通知等。
- 切点(Pointcut):切点是定义在连接点上的一个表达式,用于匹配哪些连接点需要进行增强。
通过使用切面、连接点、通知和切点等概念,AOP 可以实现对程序的横切关注点的分离和增强,提高代码的可读性、可维护性和可扩展性。
三、AOP 的使用场景
AOP 可以用于以下场景:
- 日志记录:可以在方法调用前后记录日志信息,方便进行调试和问题排查。
- 事务管理:可以在方法调用前后进行事务的开启、提交和回滚,保证数据的一致性。
- 权限控制:可以在方法调用前进行权限检查,确保只有授权的用户才能访问特定的资源。
- 性能监控:可以在方法调用前后记录性能指标,方便进行性能优化。
- 异常处理:可以在方法调用后进行异常处理,避免程序崩溃。
四、实现 AOP
Spring Framework 是一个轻量级的 Java 开发框架,它提供了对 AOP 的支持。在 Spring Framework 中,可以使用@AspectJ 注解来定义切面,使用@Pointcut 注解来定义切点,使用@Before、@After、@AfterReturning 和@AfterThrowing 等注解来定义通知。
那么今天就以日志记录来举例。
4.1新建注解@Log
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 操作模块
* @return
*/
String modul() default "";
/**
* 操作类型
* @return
*/
String type() default "";
/**
* 操作说明
* @return
*/
String desc() default "";
}
4.2新建LogAspect类
import com.alibaba.fastjson.JSON;
import com.hvit.user.util.MyUtils;
import com.hvit.user.yst.config.annotation.Log;
import com.hvit.user.yst.entity.LogErrorInfoEntity;
import com.hvit.user.yst.entity.LogInfoEntity;
import com.hvit.user.yst.service.LogErrorInfoService;
import com.hvit.user.yst.service.LogInfoService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.*;
/***
* 切面处理类,操作日志异常日志记录处理
*/
@Aspect
@Component
public class LogAspect {
/***
*项目发布版本号
*/
@Value("${version}")
private String version;
/**
* 统计请求的处理时间
*/
ThreadLocal<Long> startTime = new ThreadLocal<>();
@Autowired
private LogInfoService logInfoService;
@Autowired
private LogErrorInfoService logErrorInfoService;
/**
* @methodName:logPoinCut
* @description:设置操作日志切入点 记录操作日志 在注解的位置切入代码
* @author:caozhen
* @dateTime:2024-05-15
* @Params: []
* @Return: void
* @editNote:
*/
@Pointcut("@annotation(com.hvit.user.yst.config.annotation.Log)")
public void logPoinCut() {
}
/**
* @methodName:exceptionLogPoinCut
* @description:设置操作异常切入点记录异常日志 扫描所有controller包下操作
* @author:caozhen
* @dateTime:2024-05-15
* @Params: []
* @Return: void
* @editNote:
*/
@Pointcut("execution(* com.hvit.user.yst.controller..*.*(..))")
public void exceptionLogPoinCut() {
}
@Before("logPoinCut()")
public void doBefore() {
// 接收到请求,记录请求开始时间
startTime.set(System.currentTimeMillis());
}
/**
* @methodName:doAfterReturning
* @description:正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行
* @author:caozhen
* @dateTime:2024-05-15
* @Params: [joinPoint, keys]
* @Return: void
* @editNote:
*/
@AfterReturning(value = "logPoinCut()", returning = "keys")
public void doAfterReturning(JoinPoint joinPoint, Object keys) {
// 获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
LogInfoEntity logInfo = LogInfoEntity.builder().build();
try {
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method = signature.getMethod();
// 获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
// 获取操作
Log log = method.getAnnotation(Log.class);
if (Objects.nonNull(log)) {
logInfo.setModule(log.modul());
logInfo.setType(log.type());
logInfo.setMessage(log.desc());
}
logInfo.setMethod(className + "." + method.getName()); // 请求的方法名
logInfo.setReqParam(JSON.toJSONString(converMap(request.getParameterMap()))); // 请求参数
//logInfo.setResParam(JSON.toJSONString(keys)); // 返回结果
logInfo.setUserId(request.getAttribute("uuid") == null ? "未知" : request.getAttribute("uuid").toString()); // 请求用户ID
logInfo.setUserName(request.getAttribute("userName") == null ? "未知" : request.getAttribute("userName").toString()); // 请求用户名称
logInfo.setIp(MyUtils.getIpAddr()); // 请求IP
logInfo.setUri(request.getRequestURI()); // 请求URI
logInfo.setCreateTime(new Date()); // 创建时间
logInfo.setVersion(version); // 操作版本
logInfo.setTakeUpTime(System.currentTimeMillis() - startTime.get()); // 耗时
logInfoService.save(logInfo);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @methodName:doAfterThrowing
* @description:异常返回通知,用于拦截异常日志信息 连接点抛出异常后执行
* @author:caozhen
* @dateTime:2024-05-15
* @Params: [joinPoint, e]
* @Return: void
* @editNote:
*/
@AfterThrowing(pointcut = "exceptionLogPoinCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
// 获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
try {
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method = signature.getMethod();
// 获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
logErrorInfoService.save(
LogErrorInfoEntity.builder()
.reqParam(JSON.toJSONString(converMap(request.getParameterMap()))) // 请求参数
.method(className + "." + method.getName()) // 请求方法名
.name(e.getClass().getName()) // 异常名称
.message(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace())) // 异常信息
.userId(request.getAttribute("uuid") == null ? "未知" : request.getAttribute("uuid").toString()) // 操作员ID
.userName(request.getAttribute("userName") == null ? "未知" : request.getAttribute("userName").toString()) // 操作员名称
.uri(request.getRequestURI()) // 操作URI
.ip(MyUtils.getIpAddr()) // 操作员IP
.version(version) // 版本号
.createTime(new Date()) // 发生异常时间
.build()
);
} catch (Exception e2) {
e2.printStackTrace();
}
}
/**
* @methodName:converMap
* @description:转换request 请求参数
* @author:caozhen
* @dateTime:2024-05-15
* @Params: [paramMap]
* @Return: java.util.Map<java.lang.String, java.lang.String>
* @editNote:
*/
public Map<String, String> converMap(Map<String, String[]> paramMap) {
Map<String, String> rtnMap = new HashMap<String, String>();
for (String key : paramMap.keySet()) {
rtnMap.put(key, paramMap.get(key)[0]);
}
return rtnMap;
}
/**
* @methodName:stackTraceToString
* @description:转换异常信息为字符串
* @author:caozhen
* @dateTime:2024-05-15
* @Params: [exceptionName, exceptionMessage, elements]
* @Return: java.lang.String
* @editNote:
*/
public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
StringBuffer strbuff = new StringBuffer();
for (StackTraceElement stet : elements) {
strbuff.append(stet + "<br/>");
}
String message = exceptionName + ":" + exceptionMessage + "<br/>" + strbuff.toString();
return message;
}
}
4.1.1依赖
在LogAspect我们有用fastjson这个依赖包
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
4.1.2表结构
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_log_error_info
-- ----------------------------
DROP TABLE IF EXISTS `t_log_error_info`;
CREATE TABLE `t_log_error_info` (
`uuid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`req_param` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求参数',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '异常名称',
`message` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '异常信息',
`user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作用户id',
`user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作用户名称',
`method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求方法',
`uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求url',
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求IP',
`version` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '版本号',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`uuid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '操作日志异常信息' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for t_log_info
-- ----------------------------
DROP TABLE IF EXISTS `t_log_info`;
CREATE TABLE `t_log_info` (
`uuid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`module` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '功能模块',
`type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作类型',
`message` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作描述',
`req_param` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '请求参数',
`res_param` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '响应参数',
`take_up_time` int(10) NULL DEFAULT NULL COMMENT '耗时',
`user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作用户id',
`user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作用户名称',
`method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作方面',
`uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求url',
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求IP',
`version` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '版本号',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`uuid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '操作日志表' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
4.1.3实体类
LogInfoEntity:
import com.baomidou.mybatisplus.annotations.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.hvit.user.yst.entity.base.IdEntity;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 操作日志表
* </p>
*
* @author 曹震
* @since 2024-05-15
*/
@TableName("t_log_info")
@Data
@JsonInclude
@Builder
public class LogInfoEntity extends IdEntity implements Serializable {
public LogInfoEntity(String module, String type, String message, String reqParam, String resParam, Long takeUpTime, String userId, String userName, String method, String uri, String ip, String version, Date createTime) {
this.module = module;
this.type = type;
this.message = message;
this.reqParam = reqParam;
this.resParam = resParam;
this.takeUpTime = takeUpTime;
this.userId = userId;
this.userName = userName;
this.method = method;
this.uri = uri;
this.ip = ip;
this.version = version;
this.createTime = createTime;
}
public LogInfoEntity(){
}
private static final long serialVersionUID=1L;
/**
* 功能模块
*/
@ApiModelProperty(name = "module", value = "功能模块")
private String module;
/**
* 操作类型
*/
@ApiModelProperty(name = "type", value = "操作类型")
private String type;
/**
* 操作描述
*/
@ApiModelProperty(name = "message", value = "操作描述")
private String message;
/**
* 请求参数
*/
@ApiModelProperty(name = "reqParam", value = "请求参数")
private String reqParam;
/**
* 响应参数
*/
@ApiModelProperty(name = "resParam", value = "响应参数")
private String resParam;
/**
* 耗时
*/
@ApiModelProperty(name = "takeUpTime", value = "耗时")
private Long takeUpTime;
/**
* 操作用户id
*/
@ApiModelProperty(name = "userId", value = "操作用户id")
private String userId;
/**
* 操作用户名称
*/
@ApiModelProperty(name = "userName", value = "操作用户名称")
private String userName;
/**
* 操作方面
*/
@ApiModelProperty(name = "method", value = "操作方面")
private String method;
/**
* 请求url
*/
@ApiModelProperty(name = "uri", value = "请求url")
private String uri;
/**
* 请求IP
*/
@ApiModelProperty(name = "ip", value = "请求IP")
private String ip;
/**
* 版本号
*/
@ApiModelProperty(name = "version", value = "版本号")
private String version;
/**
* 创建时间
*/
@ApiModelProperty(name = "createTime", value = "创建时间")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss", iso = DateTimeFormat.ISO.DATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
}
LogErrorInfoEntity:
import com.baomidou.mybatisplus.annotations.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.hvit.user.yst.entity.base.IdEntity;
import io.swagger.annotations.ApiModelProperty;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 操作日志异常信息
* </p>
*
* @author 曹震
* @since 2024-05-15
*/
@TableName("t_log_error_info")
@Data
@JsonInclude
@Builder
public class LogErrorInfoEntity extends IdEntity implements Serializable {
public LogErrorInfoEntity(String reqParam, String name, String message, String userId, String userName, String method, String uri, String ip, String version, Date createTime) {
this.reqParam = reqParam;
this.name = name;
this.message = message;
this.userId = userId;
this.userName = userName;
this.method = method;
this.uri = uri;
this.ip = ip;
this.version = version;
this.createTime = createTime;
}
public LogErrorInfoEntity(){
}
private static final long serialVersionUID=1L;
/**
* 请求参数
*/
@ApiModelProperty(name = "reqParam", value = "请求参数")
private String reqParam;
/**
* 异常名称
*/
@ApiModelProperty(name = "name", value = "异常名称")
private String name;
/**
* 异常信息
*/
@ApiModelProperty(name = "message", value = "异常信息")
private String message;
/**
* 操作用户id
*/
@ApiModelProperty(name = "userId", value = "操作用户id")
private String userId;
/**
* 操作用户名称
*/
@ApiModelProperty(name = "userName", value = "操作用户名称")
private String userName;
/**
* 请求方法
*/
@ApiModelProperty(name = "method", value = "请求方法")
private String method;
/**
* 请求url
*/
@ApiModelProperty(name = "uri", value = "请求url")
private String uri;
/**
* 请求IP
*/
@ApiModelProperty(name = "ip", value = "请求IP")
private String ip;
/**
* 版本号
*/
@ApiModelProperty(name = "version", value = "版本号")
private String version;
/**
* 创建时间
*/
@ApiModelProperty(name = "createTime", value = "创建时间")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss", iso = DateTimeFormat.ISO.DATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
}
IdEntity:
import com.baomidou.mybatisplus.annotations.TableId;
import com.baomidou.mybatisplus.enums.IdType;
import java.io.Serializable;
public class IdEntity implements Serializable {
@TableId(type = IdType.INPUT)
private String uuid;
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
}
4.1.4说明
@Autowired
private LogInfoService logInfoService;
@Autowired
private LogErrorInfoService logErrorInfoService;
Service类自行建立,其实也就是mybatis-plus生成的代码。你们可以自行生成!
4.1.5怎么调用
import com.hvit.user.util.Constants;
import com.hvit.user.util.R;
import com.hvit.user.yst.config.annotation.Log;
import com.hvit.user.yst.service.AppServicesService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/***
* 测试
*/
@RestController
@RequestMapping("/xxxx/api/")
public class ApiController {
@Autowired
private AppServicesService appServicesService;
/**
* 获取二维码门牌地址
*/
@Log(modul = "日志-数字门牌对外接口", type = Constants.SELECT, desc = "数字门牌对外接口")
@RequestMapping(value = "/v1/getQrcodeAddressInfo", method = RequestMethod.GET)
@ApiOperation(value = "数字门牌对外接口")
public ResponseEntity<?> getQrcodeAddressInfo(String code, String appKey, String appSecret) {
return ResponseEntity.ok(appServicesService.getQrcodeAddressInfo(code, appKey, appSecret));
}
}
这样,只要调用了这个接口,那么正常调用日志或者异常日志都会保存进数据库中。
4.1.6版本号
代码中有获取版本号的,直接在yml文件新增如下配置
version: 2.0
五、总结
AOP 是一种强大的编程范式,它可以帮助开发者将系统级服务从业务逻辑中分离出来,提高代码的可读性、可维护性和可扩展性。在 Java 中,可以使用 Spring Framework 或 AspectJ 等框架来实现 AOP。通过使用 AOP,可以实现日志记录、事务管理、权限控制、性能监控和异常处理等功能,使代码更加健壮和易于维护。