Spring通过切面记录日志

这两天项目用户越来越多,决定给控制层统一加上日志,方便对出现的问题数据进行原因查找。在加日志的过程中遇到了一些小麻烦(如何获取方法参数名,内容,打印到指定文件,不再控制台打印等),在这里记录一下。
首先web.xml下要不要加以下配置,看你的log4j配置文件是不是在spring的约定位置,如果是的话可加可不加。假如你要加的话要注意的是Log4jConfigListener必须要在Spring的Listener之前。

<!--打印日志配置文件位置,可以不配置则去默认位置搜索,搜索不到则使用tomcat默认的日志配置所以此处log4j.properties找不到时,此xml文件依旧不会报错-->
  <!--但是只会扫面src目录(包括子目录)下是否有此文件(先去找log4j.xml,然后再去找log4j.properties)-->
  <context-param>
    <param-name>log4jConfigLoaction</param-name>
    <param-value>classpath:log4j.properties</param-value>
  </context-param>
  <!-- 加载Spring框架中的log4j监听器Log4jConfigListener -->
  <listener>
    <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
  </listener>

之前在查询资料的时候发现有人指出log4j.properties和log4j.xml的约定位置不同,说log4j.xml在resources下,log4j.properties在WEB-INF下。我没有发现这个问题,我的log4j.properties在resources下,也并没有配置log4jConfigLoaction,依旧可用。

下面是log4j.properties中的配置(底部有部分日志的配置解释!底部有部分日志的配置解释!底部有部分日志的配置解释!)

#log4j.properties加载是自动的但是只会扫面src目录(包括子目录)下是否有此文件(先去找log4j.xml,然后再去找log4j.properties)可在web.xml中配置配置文件位置
#<context-param><param-name>log4jConfigLoaction</param-name><param-value>classpath:log4j.properties</param-value></context-param>

#日志等级由高到低分别为 OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL  (简介见底部1)
#Log4j建议只使用四个级别,优先级从高到低分别是 ERROR、WARN、INFO、DEBUG
#程序会打印高于或等于所设置级别的日志,设置的日志等级越高,打印出来的日志就越少。
#下面配置日志的级别和输出的配置  第一个为日志级别,后面的是输出配置(名字可自定义,多个输出配置用逗号隔开)
log4j.rootLogger=ERROR,stdout
#下面将定义定义名为stdout的输出端是哪种类型(详情见底部2)
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
#下面将定义名为stdout的输出端的layout是哪种类型(详情见底部3)
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
#如果使用pattern布局就要指定的打印信息的具体格式ConversionPattern(详情见底部4)
log4j.appender.stdout.layout.ConversionPattern=%d %-5p %l - %m%n
#打印sql的 目前没研究明白 因还有一些配置不知在哪配置:包括是否输出查询结果 而且mapper中也要配上注解
org.apache.ibatis.logging.LogFactory.useLog4JLogging();
#设置sql日志只打印sql不打印查询结果
#配置丢失,请自行查询

#*************************************************************这里是新加的日志配置*******start***************************************
log4j.logger.org.redblue.hide.controller.webutil.LogsAspect=info,lanlytest
#log4j.additivity是子Logger是否继承父Logger的输出源(appender)的标志位。如果继承了那么打印到这里的同时也会在父输出源打印一份<这里必须写路径而不是日志别名lanlytest>
#log4j.additivity.org.redblue.hide.controller.webutil.LogsAspect = false
log4j.appender.lanlytest=org.apache.log4j.DailyRollingFileAppender
#如果你使用的是tomcat,那么默认就可以使用${catalina.home}获得tomcat的根目录
#log4j.appender.lanlytest.File=${catalina.home}/logs/firestorm.log
#windos下需使用绝对路径不然会有问题
log4j.appender.lanlytest.File=D\:/lanly/worktool/workspace/ideal-tomcat-log4j-output/logs/firestorm.log
log4j.appender.lanlytest.MaxFileSize=100KB
log4j.appender.lanlytest.MaxBackupIndex=1
#在日志后面追加还是覆盖,默认是true追加
log4j.appender.lanlytest.Append = true
log4j.appender.lanlytest.layout=org.apache.log4j.PatternLayout
log4j.appender.lanlytest.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} - %m%n
#*************************************************************这里是新加的日志配置*******end***************************************

#1/2/3/4分别对应上面加标注的位置
#1
#日志等级简介
#DEBUG Level指出细粒度信息事件对调试应用程序是非常有帮助的。
#INFO level表明 消息在粗粒度级别上突出强调应用程序的运行过程。
#WARN level表明会出现潜在错误的情形。
#ERROR level指出虽然发生错误事件,但仍然不影响系统的继续运行。
#FATAL level指出每个严重的错误事件将会导致应用程序的退出。
#另外,还有两个可用的特别的日志记录级别: (以下描述来自log4j API http://jakarta.apache.org/log4j/docs/api/index.html):
#ALL Level是最低等级的,用于打开所有日志记录。
#OFF Level是最高等级的,用于关闭所有日志记录。

#2
#输出端是类型
#org.apache.log4j.ConsoleAppender(控制台),
#org.apache.log4j.FileAppender(文件),
#org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件),
#org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
#org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)

#3
#此句为定义名为stdout的输出端的layout是哪种类型
#org.apache.log4j.HTMLLayout(以HTML表格形式布局)
#org.apache.log4j.PatternLayout(可以灵活地指定布局模式)
#org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)
#org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)

#4
#log4j.appender.stdout.layout.ConversionPattern= [QC] %p [%t] %C.%M(%L) | %m%n
#如果使用pattern布局就要指定的打印信息的具体格式ConversionPattern,打印参数如下:
#%m 输出代码中指定的消息(为了方便你自己查找错误位置可以自己加内容,当然也可以在代码中加固定的日志消息<但多人开发可能会忘记加日志特定消息>)
#%p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL
#%r 输出自应用启动到输出该log信息耗费的毫秒数
#%c 输出所属的类目,通常就是所在类的全名
#%t 输出产生该日志事件的线程名
#%n 输出一个回车换行符,Windows平台为“rn”,Unix平台为“n”
#%d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921
#%l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。
#[QC]是log信息的开头,可以为任意字符,一般为项目简称。

接下来就是用来记录日志的切面类(里面提供了一些获取参数名以及参数内容的方法,欢迎使用和交流)

package org.redblue.hide.controller.webutil;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.aspectj.lang.JoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.ThrowsAdvice;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @Author lanly
 * @Date 2018/12/25 0025.
 */
public class LogsAspect implements MethodBeforeAdvice, AfterReturningAdvice, ThrowsAdvice {
    private  Logger logger = LoggerFactory.getLogger(LogsAspect.class);

    public LogsAspect() {
    }

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {}
    public void after(JoinPoint point) {}

    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        logger.info(getLogString(method,args,target,null));
    }

//这里没有@Override 发现ThrowsAdvice 接口中并没有任何方法,Spirng内部是用反射来实现方法匹配的,需要实现下列接口中的其中1个
/**
 *
 *  public void afterThrowing(Exception ex)
 *  public void afterThrowing(RemoteException)
 *  public void afterThrowing(Method method, Object[] args, Object target, Exception ex)
 *  public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)
 * */
    public void afterThrowing(Method method, Object[] args, Object target, Exception ex) throws Throwable {
        logger.info(getLogString(method,args,target,ex));
    }

    private static String getLogString(Method method, Object[] args, Object target, Exception ex)throws Throwable{
        StringBuilder logString = new StringBuilder();
        String className = target.getClass().getName();
        String methodName = method.getName();
        //这里不可以重定义logger因为重定义之后专门记录切面日志的输出将认为这不是切面的日志而是重新定义的类的日志
        // logger = LoggerFactory.getLogger(target.getClass());
        List<String> paramNames = getParamterName(target.getClass(),methodName);
        List<String> paramValues = new ArrayList<String>();
        StringBuilder paramsString = new StringBuilder();
        try {
            String value = "null";
            //获取所有的参数
            for (int k = 0; k < args.length; k++) {
                Object arg = args[k];
                if(arg!=null){
                    String argTypeName = arg.getClass().getTypeName();
                    if(isPrimite(argTypeName)){
                        value = new String(arg+"");
                    }else{
                    //这里使用SerializerFeature.IgnoreNonFieldGetter来防止alibaba.fastjson转换部分对象时出现异常情况
                        value = JSON.toJSONString(arg, SerializerFeature.IgnoreNonFieldGetter);
                    }
                    paramValues.add(value);
                }else{
                    paramValues.add("null");
                }

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        for (int i =0 ;i<paramNames.size();i++) {
            paramsString.append(paramNames.get(i)+":"+paramValues.get(i)+",");
        }
        logString.append("className ="+className+";    methodName = "+methodName+";");
        if(paramsString.length()>0){
            logString.append("    args="+paramsString.substring(0,paramsString.length()-1)+";");
        }
        if(ex!=null){
            logString.append("     exception="+ex.getMessage()+";");
        }
        return logString.toString();
    }
    /**
     * 判断是否为基本类型:包括String和基本类型封装类
     * @param typeName clazz
     * @return  true:是;     false:不是
     */
    private static boolean isPrimite(String typeName){
        for (String t : types) {
            if (t.equals(typeName)) {
                return true;
            }
        }
        return false;
    }

    private static String[] types = {"java.lang.Integer", "java.lang.Double",
            "java.lang.Float", "java.lang.Long", "java.lang.Short",
            "java.lang.Byte", "java.lang.Boolean", "java.lang.Char",
            "java.lang.String", "int", "double", "long", "short", "byte",
            "boolean", "char", "float"
    };
   //获取方法参数名
        public static List<String> getParamterName(Class clazz, String methodName){
            LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
            Method[] methods = clazz.getDeclaredMethods();
            for (Method method : methods) {
                if (methodName.equals(method.getName())) {
                    String[] params = u.getParameterNames(method);
                    return Arrays.asList(params);
                }
            }
            return null;
        }
    }

遇到的问题包括,获取参数名,获取参数内容并转为字符串,控制台有日志打印却输出不到指定文件。问题都在文章中解决了,另外输出都指定文件时因为多次测试发现log4j的配置文件在修改后立刻启动可能会存在未保存修改的情况,虽然ideal会自动保存但是有时间间隔,所以建议每次修改后及时保存。

猜你喜欢

转载自blog.csdn.net/codepro_w_/article/details/85274243
今日推荐