Java 切面增强版

3 篇文章 0 订阅

前言

    众说周知,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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值