本篇文章教给大家自定义日志注解功能的实现,主要技术核心是Spring-AOP。
一、AOP回顾
AOP 采取横向抽取机制(动态代理),取代了传统纵向继承机制的重复性代码,其应用主要体现在事务处理、日志管理、权限控制、异常处理等方面。主要作用是分离功能性需求和非功能性需求,使开发人员可以集中处理某一个关注点或者横切逻辑,减少对业务代码的侵入,增强代码的可读性和可维护性。简单的说,AOP 的作用就是保证开发者在不修改源代码的前提下,为系统中的业务组件添加某种通用功能。
其核心概念包括JoinPoint、PointCut、Advice、Target、Weaving、Proxy、Aspect,除此之外还有before、after、after-returning、after-throwing、around。
具体详细内容不在此赘述,忘记aop或者不了解aop的可以搜索相关文章进行学习。
二、编写简单业务
因为功能过于简单,本部分只进行简略的代码介绍。
1. 程序结构
2. 基本代码展示
annotation
包下代码在后续内容介绍。
constant
包下的BizType
即业务功能枚举,不在赘述。
domain
包下的MsgVo
表示前端传递数据结构,R
表示通用响应体。
综上,下面仅展示TestController
和LogInfo
两个文件的代码。
// LogInfo.java - 存储日志的结构
package com.darrin.logdemo.domain;
import com.darrin.logdemo.constant.BizType;
import lombok.Data;
@Data
public class LogInfo {
// 业务请求地址
private String url;
// 业务处理结果 - 业务处理触发catch捕获则为处理失败 应当存放报错信息
private boolean status;
// 若业务处理失败 该字段存放报错内容
private String errorMsg;
// 业务请求函数名
private String method;
// 业务请求的HTTP方法 - GET/PUT/POST/DELETE
private String requestType;
// 自定义业务字段
private BizType bizType;
// 日志标题
private String title;
// 业务方法参数
private String param;
// 业务方法执行结果
private String result;
}
// TestController.java - 简单的增删改查功能
// 利用静态生成的HashMap代替实际业务的数据库db,代替实现增删改查的效果
package com.darrin.logdemo.controller;
import com.darrin.logdemo.constant.BizType;
import com.darrin.logdemo.domain.MsgVo;
import com.darrin.logdemo.domain.R;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
@RestController
@RequestMapping("/test")
public class TestController {
private static HashMap<String, String> data = new HashMap<>();
static {
data.put("admin", "adminPwd");
data.put("test", "testPwd");
data.put("darrin", "darrinPwd");
data.put("language", "Java");
}
@PostMapping
public R test_add(@RequestBody MsgVo msgVo) {
String k = msgVo.getKey();
String v = msgVo.getValue();
data.put(k, v);
return R.ocm(null, "200", "创建成功");
}
@DeleteMapping
public R test_delete(@RequestBody MsgVo msgVo) {
String k = msgVo.getKey();
if (data.containsKey(k)) {
data.remove(k);
return R.ocm(null, "200", "删除成功");
} else {
return R.ocm(null, "1000", "不存在该数据, 无法删除");
}
}
@PutMapping
public R test_update(@RequestBody MsgVo msgVo) {
String k = msgVo.getKey();
if (data.containsKey(k)) {
String v = data.get(k);
data.put(k, msgVo.getValue());
return R.ocm(v + "->" + data.get(k), "200", "已完成更新");
} else {
return R.ocm(null, "1001", "不存在该数据, 无法更新");
}
}
@GetMapping
public R test_query(MsgVo msgVo) {
String k = msgVo.getKey();
if (data.containsKey(k)) {
String v = data.get(k);
return R.ocm(v, "200", "查询成功");
} else {
return R.ocm(null, "1002", "不存在该数据");
}
}
}
三、注解功能实现
1. 日志内容设计
首先需要考虑日志需要存储什么信息,上文提到存储了各种内容。不难发现其中有部分字段存储的信息可以由程序运行时由代码进行填充,如参数和响应等等;但是也有部分信息存在主观性,比如title标题,毕竟将日志存储到数据库的目的是被维护人员查阅的,所以仍然需要我们去手动填充一些字段信息。
本篇文章我们姑且认为title
和bizType
需要我们人为干预填写。所以编写代码文件
package com.darrin.logdemo.annotation;
import com.darrin.logdemo.constant.BizType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
/**
* 日志标题
*/
String title() default "";
/**
* 业务类型
*/
BizType type() default BizType.OTHER;
}
我们可以看到此处编写的代码与过去编写的有所不同,且存在default
关键字,所以大家编写自己的自定义注解时可以设计某个字段的默认值,然后根据值的最终结果再进行不同的响应处理。常见的比如说boolean
类型的是否进行数据脱敏、是否不存放入参出参(参数可能非常占用硬盘空间)等等。
2. 切面代码编写
在开始这部分内容之前,还是需要理解aop的概念(因为日志实现本就没有什么困难)。
官方的说,aop就是将代码切出一个切面,在其上下进行额外功能赋予,这样可以在不改变原本代码结构的情况下实现功能的拓展。
通俗一点的讲,aop大概是这个样子:
我们有一个方法叫hello()
,那么代码调用的时候就是直接执行hello()
。倘若我们使用aop思想给他了一个切面并编写了方法,那么执行顺序就是这样的:
下达调用hello()方法的命令 -> 执行用
before
修饰的方法 -> 执行hello()
-> 执行用after
修饰的方法
思路非常的简单,那么日志功能代码也就直接编写出来了。
@Before(value = "@annotation(controllerLog)")
public void doBefore(JoinPoint joinPoint, Log controllerLog) {
// 不执行操作
}
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
handleLog(joinPoint, controllerLog, null, jsonResult);
}
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
handleLog(joinPoint, controllerLog, e, null);
}
既然是简单的日志处理,那么在业务代码执行之前我们似乎没有需要存储的东西,所以@Before
部分是空的,在查阅其他人的日志注解编写时发现可以在此处调用map存放一个系统时间,在业务处理完之后比对系统时间从而获得业务处理消耗的时间,本处就不进行编写,只提供一个@before部分的内容思路,以便有更好的需求实现。
@AfterReturning指的是功能顺利调用之后的结果,@AfterThrowing则是调用失败的结果。他们均调用了handleLog
方法,该方法是获取并生成日志信息的核心方法,后两个参数分别是报错信息和返回结果。
private void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
try {
// 1. 创建日志
LogInfo logInfo = new LogInfo();
// 2. 确定本次操作状态, 若存在报错信息则表示操作失败
logInfo.setStatus(e == null);
// 获取request, 以便获取请求信息
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.
getRequestAttributes()).getRequest();
// 3. 存储IP - 篇幅过长不进行实现, 如有需求可以了解相关内容, 功能实现也是由request字段获取
// 4. 存储URI
logInfo.setUrl(request.getRequestURI());
// 5. 存储请求方式
logInfo.setRequestType(request.getMethod());
// 6. 存储调用方法 - 切点的业务处理, 可以获取类名和方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
// 可以让方法名更美观
String callMethodName = className + "." + methodName + "()";
logInfo.setMethod(callMethodName);
// 7. 存储参数信息 - 调用方式
logInfo.setBizType(controllerLog.type());
// 8. 存储参数信息 - 标题
logInfo.setTitle(controllerLog.title());
// 9. 存储参数信息 - 请求参数
logInfo.setParam(JSON.toJSONString(joinPoint.getArgs()));
// 10. 存储响应信息
logInfo.setResult(JSON.toJSONString(jsonResult));
// 11. 日志存放处理 - 此处直接将其打印, 实际需求可以走数据库进行日志存放
System.out.println(logInfo);
} catch (Exception exception) {
exception.printStackTrace();
}
}
四、功能测试
由于没有编写前端代码,直接使用api测试工具测试。
1. POST
// 业务代码 - post
@Log(title = "添加", type = BizType.ADD)
@PostMapping
public R test_add(@RequestBody MsgVo msgVo) {
String k = msgVo.getKey();
String v = msgVo.getValue();
data.put(k, v);
return R.ocm(null, "200", "创建成功");
}
// 传入参数
{
"key" : "thisIsKey",
"value" : "thisIsValue"
}
// 返回结果
{
"object": null,
"code": "200",
"msg": "创建成功"
}
// 控制台打印结果
LogInfo(url=/test, status=true, errorMsg=null, method=com.darrin.logdemo.controller.TestController.test_add(), requestType=POST, bizType=ADD, title=添加, param=[{"key":"thisIsKey","value":"thisIsValue"}], result={"code":"200","msg":"创建成功"})
2. GET
// 业务代码 - GET
@Log(title = "查询", type = BizType.QUERY)
@GetMapping
public R test_query(MsgVo msgVo) {
String k = msgVo.getKey();
if (data.containsKey(k)) {
String v = data.get(k);
return R.ocm(v, "200", "查询成功");
} else {
return R.ocm(null, "1002", "不存在该数据");
}
}
// 请求地址
http://localhost:8080/test?key=thisIsKey
// 返回结果
{
"object": "thisIsValue",
"code": "200",
"msg": "查询成功"
}
// 控制台打印结果
LogInfo(url=/test, status=true, errorMsg=null, method=com.darrin.logdemo.controller.TestController.test_query(), requestType=GET, bizType=QUERY, title=查询, param=[{"key":"thisIsKey"}], result={"code":"200","msg":"查询成功","object":"thisIsValue"})
3. PUT
// 业务代码 - put
@Log(title = "修改", type = BizType.UPDATE)
@PutMapping
public R test_update(@RequestBody MsgVo msgVo) {
String k = msgVo.getKey();
if (data.containsKey(k)) {
String v = data.get(k);
data.put(k, msgVo.getValue());
return R.ocm(v + "->" + data.get(k), "200", "已完成更新");
} else {
return R.ocm(null, "1001", "不存在该数据, 无法更新");
}
}
// 传入参数
{
"key" : "thisIsKey",
"value" : "thisIsNewValue"
}
// 返回结果
{
"object": "thisIsValue->thisIsNewValue",
"code": "200",
"msg": "已完成更新"
}
// 控制台打印结果
LogInfo(url=/test, status=true, errorMsg=null, method=com.darrin.logdemo.controller.TestController.test_update(), requestType=PUT, bizType=UPDATE, title=修改, param=[{"key":"thisIsKey","value":"thisIsNewValue"}], result={"code":"200","msg":"已完成更新","object":"thisIsValue->thisIsNewValue"})
4. DELETE
// 业务代码 - delete
@Log(title = "删除", type = BizType.DELETE)
@DeleteMapping
public R test_delete(@RequestBody MsgVo msgVo) {
String k = msgVo.getKey();
if (data.containsKey(k)) {
data.remove(k);
return R.ocm(null, "200", "删除成功");
} else {
return R.ocm(null, "1000", "不存在该数据, 无法删除");
}
}
// 传入参数
{
"key" : "thisIsKey",
"value" : "thisIsNewValue"
}
// 返回结果
{
"object": null,
"code": "200",
"msg": "删除成功"
}
// 控制台打印结果
LogInfo(url=/test, status=true, errorMsg=null, method=com.darrin.logdemo.controller.TestController.test_delete(), requestType=DELETE, bizType=DELETE, title=删除, param=[{"key":"thisIsKey","value":"thisIsNewValue"}], result={"code":"200","msg":"删除成功"})