前言
众说周知,aop是oop思想的延续,是为了我们更好的程序的开发更便于我们对技术及代码的维护。
今天就利用aop来做一个日志的记录。废话不多说,上代码。
代码
package com.wind.sky.util;
import com.wind.sky.annotation.LoggerRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Component
@Aspect
public class LoggerRecordAop implements Serializable {
private static final Logger logger = LoggerFactory.getLogger(LoggerRecordAop.class);
private final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 规定日期格式
private static final long serialVersionUID = 8147703597827139556L;
@Resource
private HttpServletRequest request;
@Resource
private HttpServletResponse response;
@Resource
private JdbcTemplate jdbcTemplate;
@Resource
private LoggerService loggerService;
private ThreadLocal<LoggerRecord> loggerRecordThread = new ThreadLocal<LoggerRecord>(); //防止多线程导致的日志被重新赋值
LoggerRecord loggerRecord=new LoggerRecord(); ①
@Pointcut(value= "execution (* com.XXX.XXX.controller.XXXController.*(..))")
// @Pointcut("@annotation(LoggerAop)")
public void recordApiLogger(){
/* *
*1.先定义一个切入点
* 2.可以匹配注解
* 3.可以用 execution 表达式
* 注解的执行顺序
* @before @around @after ②
* */
}
/**
*@Description : 可以获取一些常用参数,非密切关联的 比如 ip,url ,interface_desc
*/
@Before(value="recordApiLogger()")
public void getRequestBaseInfo(){
loggerRecordThread.set(loggerRecord);//
String remoteAddr = request.getRemoteAddr();//获取请求地址
String url = request.getRequestURL().toString();
String headParam = request.getParameter("XXXXX"); //获取项目请求头的参数
}
@Around(value = "recordApiLogger()")
public Object saveRecordApiLogger(ProceedingJoinPoint joinPoint){
long startTime = System.currentTimeMillis();
Object retVal = joinPoint.proceed();
long entTime = System.currentTimeMillis();
String consumeTime = formatDuring(entTime - startTime);
Object[] args = joinPoint.getArgs();//
List<Object> validArgs =new ArrayList<>(); //可以用来记录的非接口类型参数
for (Object arg: args){
if(arg instanceof MultipartFile || arg instanceof HttpServletRequest || arg instanceof HttpServletResponse){
continue;
}else{
validArgs.add(arg);
}
}
// validArgs 是入参,可以通过反射获取所需要的具体值
//retVal 是返回的对象,响应的结果,可以强转拿到自己所需的参数
for (int i = 0; i < validArgs.size(); i++) {
if(validArgs.get(i) instanceof Object){
Class userCla = validArgs.get(i).getClass();
// getFields():获得某个类的所有的公共(public)的字段,包括父类中的字段。
// getDeclaredFields():获得某个类的所有声明的字段,即包括public、private和proteced,但是不包括父类的申明字段
Field[] fs = userCla.getDeclaredFields();
for (Field f : fs) {
f.setAccessible(true);//修改访问private修饰的权限
if ("XXXXXX".equals(f.getName())) {
try {
Object value = f.get(args[i]); //获取当前属性的值
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
return retVal; //必须要返回,不然所有的资源会被拦截
}
/**
*@Description : @AfterThrowing 异常通知用来抓取异常,前提是异常被抛出来,如果Try catch 是进不来的 可以选择其他方式获取所需
*@Param:
*@return:
*/
@AfterThrowing(value="recordApiLogger()",throwing = "e")
public void recordExceptionInfo(JoinPoint joinPoint,Throwable e){
//错误信息匹配记录
if(e instanceof MessageException){
loggerRecord.setMessage(e.getMessage()); //失败提醒信息
}else if (e instanceof NullPointerException ){
loggerRecord.setMessage("空指针异常,详情查看error记录");
}else if( e instanceof ArithmeticException){
loggerRecord.setMessage("算术异常类异常,详情查看error记录");
}else if(e instanceof ArrayIndexOutOfBoundsException ){
loggerRecord.setMessage("索引越界异常,详情查看error记录");
}else if( e instanceof ClassCastException){
loggerRecord.setMessage("类强转异常,详情查看error记录");
}else if (e instanceof NumberFormatException){
loggerRecord.setMessage("数据格式转换异常,详情查看error记录");
}else if(e instanceof SQLException){
loggerRecord.setMessage("数据库SQL异常,详情查看error记录");
}else if(e instanceof RuntimeException) {
loggerRecord.setMessage(e.getMessage().substring(0, 300));
}
loggerRecord.setError(e.toString().length()<1000?e.toString():e.toString().substring(0,1001));//程序异常信息的打印
}
/**
*@Description :@After 无论如何都会走进来,做一些必须在最后操作的逻辑
*/
@After(value="recordApiLogger()")
public void getResult(){
LoggerRecord loggerRecord = loggerRecordThread.get();//先得到
String createTime = df.format(new Date());
try{
//保存日志
String insertSql = " insert table values() ";
jdbcTemplate.update(insertSql,Object ... args); //方式1
loggerService.save(loggerRecord); //方式2
} catch (DataAccessException e) {
logger.error("日志入库失败: {}",e.getCause());
} finally {
loggerRecordThread.remove();//一定要手动释放内存
}
}
//请求耗时计算
public static String formatDuring(long time) {
long minutes = (time % (1000 * 60 * 60)) / (1000 * 60);
long seconds = (time % (1000 * 60)) / 1000;
long millisecond = time % 1000;
return "共耗时"+time+"毫秒,转为时分秒为: "+ minutes + "分钟," + seconds + "秒," + millisecond + "毫秒 ";
}
class MessageException extends RuntimeException{
}
}
注意
① 处有线程安全问题
② 处顺序不对
处理参考方案
①处的线程安全问题
A:如果本切面只用一个方法写到这个全局位置是没有问题的,而且也不需 ThreadLocal<LoggerRecord> loggerRecordThread 来进行线程隔离
B:因为本切面有多个方法,并发情况下就会导致切面日志对象的数据不一致性,此时必须使用ThreadLocal来进行线程隔离
C: 尽管用了ThreadLocal,这只是第一步,本切面这样就会导致数据不一致问题的出现,日志对象不应该是全局的,应该做为局部变量来使用,可以有两个方案解决:
方案一:
private ThreadLocal<LoggerRecord> loggerRecordThread = new ThreadLocal<LoggerRecord>(); //防止多线程导致的日志被重新赋值
@Around(value = "recordApiLogger()")
public Object saveRecordApiLogger(ProceedingJoinPoint joinPoint){
// LoggerRecord loggerRecord=new LoggerRecord(); ①
LoggerRecord loggerRecord=new LoggerRecord(); //①位置改为局部
getBaseReqInfo(loggerRecord); //前置拦截作为一个私有方法
long startTime = System.currentTimeMillis();
Object retVal = joinPoint.proceed();
long entTime = System.currentTimeMillis();
String consumeTime = formatDuring(entTime - startTime);
Object[] args = joinPoint.getArgs();//
List<Object> validArgs =new ArrayList<>(); //可以用来记录的非接口类型参数
...
}
//抽取一个私有方法
private LoggerRecord void getBaseReqInfo(LoggerRecord loggerRecord) {
String remoteAddr = request.getRemoteAddr();//获取请求地址
String url = request.getRequestURL().toString();
String headParam = request.getParameter("XXXXX"); //获取项目请求头的参数
loggerRecord.setRemoteAddr(remoteAddr);
loggerRecord.setUrl(url);
loggerRecord.setHeadParam(headParam);
loggerRecordThread.set(loggerRecord);
}
方案二:
/**
*@Description : 源代码不变的情况下,二次赋值ThreadLocal
*/
@Before(value="recordApiLogger()")
public void getRequestBaseInfo(){
// loggerRecordThread.set(loggerRecord);//
if(null==loggerRecordThread.get()){
loggerRecordThread.set(new LoggerRecord());
loggerRecord=loggerRecordThread.get();
}
String remoteAddr = request.getRemoteAddr();//获取请求地址
String url = request.getRequestURL().toString();
String headParam = request.getParameter("XXXXX"); //获取项目请求头的参数
}
对于②处的顺序问题
A: 这个自己调试一下就会真相大白
B: @Around(value = "recordApiLogger()") 环绕通知是最先进来的,当执行到 joinPoint.proceed();才是按之前的顺序执行
姊妹篇
https://blog.csdn.net/WindwirdBird/article/details/105606701