本文参考EL-ADMIN 后台管理系统,学习相关AOP(Aspect-Oriented Programming:面向切面编程),并实现使用AOP进行日志管理。特别感谢该项目的源代码作者:elunez
文章目录
- 一、准备阶段
- 二、编码阶段
- 三、使用阶段
一、准备阶段
SpringBoot中需要先引入aop依赖
<!-- Mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.4.1</version>
</dependency>
<!-- aop切面 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- ip处理 -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>1.7.2</version>
</dependency>
<!-- 解析客户端操作系统、浏览器信息 -->
<dependency>
<groupId>nl.basjes.parse.useragent</groupId>
<artifactId>yauaa</artifactId>
<version>5.23</version>
</dependency>
创建一张sys_log的日志表
CREATE TABLE `sys_log` (
`log_id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`log_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`method` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`params` text CHARACTER SET utf8 COLLATE utf8_general_ci,
`request_ip` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`time` bigint DEFAULT NULL,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`browser` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`exception_detail` text CHARACTER SET utf8 COLLATE utf8_general_ci,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`log_id`) USING BTREE,
KEY `log_create_time_index` (`create_time`) USING BTREE,
KEY `inx_log_type` (`log_type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3537 DEFAULT CHARSET=utf8mb3 COMMENT='系统日志';
二、编码阶段
日志工具类
/**
* @author: Vainycos
* @description 日志服务工具类
* @date: 2021/9/29 15:07
*/
@Slf4j
public class LogUtils {
private static boolean ipLocal = false;
private static File file = null;
private static DbConfig config;
// IP归属地查询
public static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp?ip=%s&json=true";
/**
* 根据ip获取详细地址
*/
public static String getCityInfo(String ip) {
if (ipLocal) {
return getLocalCityInfo(ip);
} else {
return getHttpCityInfo(ip);
}
}
/**
* 根据ip获取详细地址
*/
private static String getLocalCityInfo(String ip) {
try {
DataBlock dataBlock = new DbSearcher(config, file.getPath())
.binarySearch(ip);
String region = dataBlock.getRegion();
String address = region.replace("0|", "");
char symbol = '|';
if (address.charAt(address.length() - 1) == symbol) {
address = address.substring(0, address.length() - 1);
}
return address;
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return "";
}
/**
* 根据ip获取详细地址
*/
private static String getHttpCityInfo(String ip) {
String api = String.format(IP_URL, ip);
JSONObject object = JSONUtil.parseObj(HttpUtil.get(api));
return object.get("addr", String.class);
}
}
日志实体类
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("sys_log")
public class LogDO {
private static final long serialVersionUID = 1L;
/**
* 唯一标识
*/
@TableId(value = "log_id", type = IdType.AUTO)
private Long id;
/** 操作用户 */
private String username;
/** 描述 */
private String description;
/** 方法名 */
private String method;
/** 参数 */
private String params;
/** 日志类型 */
private String logType;
/** 请求ip */
private String requestIp;
/** 地址 */
private String address;
/** 浏览器 */
private String browser;
/** 请求耗时 */
private Long time;
/** 异常详细 */
private byte[] exceptionDetail;
/** 创建日期 */
private Timestamp createTime;
public LogDO(String logType, Long time) {
this.logType = logType;
this.time = time;
}
}
Serivce类
public interface LogService {
/**
* 保存日志数据
* @param username 用户
* @param browser 浏览器
* @param ip 请求IP
* @param joinPoint /
* @param log 日志实体
*/
@Async
void save(String username, String browser, String ip, ProceedingJoinPoint joinPoint, LogDO log);
}
Service实现类
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogMapper logMapper;
@Override
public void save(String username, String browser, String ip, ProceedingJoinPoint joinPoint, LogDO log) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
MyLog aopLog = method.getAnnotation(MyLog.class);
// 方法路径
String methodName = joinPoint.getTarget().getClass().getName() + "." + signature.getName() + "()";
// 描述
if (log != null) {
log.setDescription(aopLog.value());
}
assert log != null;
log.setRequestIp(ip);
log.setAddress(LogUtils.getCityInfo(log.getRequestIp()));
log.setMethod(methodName);
log.setUsername(username);
log.setParams(getParameter(method, joinPoint.getArgs()));
log.setBrowser(browser);
logMapper.insert(log);
}
/**
* 根据方法和传入的参数获取请求参数
*/
private String getParameter(Method method, Object[] args) {
List<Object> argList = new ArrayList<>();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
//将RequestBody注解修饰的参数作为请求参数
RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
if (requestBody != null) {
argList.add(args[i]);
}
//将RequestParam注解修饰的参数作为请求参数
RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
if (requestParam != null) {
Map<String, Object> map = new HashMap<>();
String key = parameters[i].getName();
if (!StringUtils.isEmpty(requestParam.value())) {
key = requestParam.value();
}
map.put(key, args[i]);
argList.add(map);
}
}
if (argList.size() == 0) {
return "";
}
return argList.size() == 1 ? JSONUtil.toJsonStr(argList.get(0)) : JSONUtil.toJsonStr(argList);
}
}
Mapper接口(此处使用了Mybatis-plus)
@Repository
public interface LogMapper extends BaseMapper<LogDO> {
}
创建annotation包,创建Log注解类
public @interface MyLog {
String value() default "";
}
创建aspect包,创建LogAspect的日志切面类,并加上@Aspect注解标记为切面
@Component
@Aspect
@Slf4j
public class LogAspect {
ThreadLocal<Long> currentTime = new ThreadLocal<>();
@Autowired
private LogService logService;
}
在LogAspect切面类中,需要补充以下方法:
/**
* 配置切入点
*/
@Pointcut("@annotation(com.xxx.MyLog)")
public void logPointcut(){
// 无方法体,主要在@Pointcut中体现@Log注解类的所在位置
}
/**
* 配置环绕通知,使用在方法logPoint()上注册切入点
*/
@Around("logPointcut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object result;
currentTime.set(System.currentTimeMillis());
result = joinPoint.proceed();
LogDO logDO = new LogDO("INFO", System.currentTimeMillis() - currentTime.get());
currentTime.remove();
HttpServletRequest request = getHttpServletRequest();
logService.save("用户姓名xxx", LogUtils.getBrowser(request), LogUtils.getIp(request),joinPoint, logDO);
return result;
}
/**
* 配置异常通知
*
* @param joinPoint join point for advice
* @param e exception
*/
@AfterThrowing(pointcut = "logPointcut()", throwing = "e")
public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
LogDO log = new LogDO("ERROR",System.currentTimeMillis() - currentTime.get());
currentTime.remove();
log.setExceptionDetail(getStackTrace(e).getBytes());
HttpServletRequest request = getHttpServletRequest();
logService.save("用户姓名xxx", LogUtils.getBrowser(request), LogUtils.getIp(request), (ProceedingJoinPoint)joinPoint, log);
}
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}
/**
* 获取堆栈信息
*/
public static String getStackTrace(Throwable throwable){
StringWriter sw = new StringWriter();
try (PrintWriter pw = new PrintWriter(sw)) {
throwable.printStackTrace(pw);
return sw.toString();
}
}
三、使用阶段
在对应的Controller类的方法上,加上自定义的@MyLog注解,并写上该方法的简短说明。
访问/test/go,即可在sys_log表中看到日志记录的信息。
@RestController
@RequestMapping("/test")
public class TestController {
@MyLog("测试方法")
@GetMapping("/go")
String go(){
return "日志记录成功!";
}
}
后续若有时间将会实现一个简单的切面日志,并开放代码。
参考资料: