在Spring中轻松写日志

最近觉得写的一点代码(JAVA),还觉得颇为自得,贡献出来供大家参考。

首先,先上代码:

@Controller
public class Controller1{

     @WriteLog(value = "${p0.username}从${ctx.ip}登录, 登录${iif(ret.success,‘成功‘,‘失败‘)}")
     public Object login(Login loginObj, HttpServletRequest req){
         //blablabla...
     }
}

在代码中,给login方法加上@WriteLog——相当于C#的[WriteLog],当代码运行了login方法时,spring就会自动记录日志——而日志内容则是会自动替换其中${}的占位符号。

如上就会记录日志:“.net bean从127.0.0.1登录,登录成功”

其它地方想要怎么记日志,也是如此,比起原来

public class ClassA{
    private static final Log log = LogFactory.getLog(ClassA.class);

    public void Method(String p1, String p2){
        log.info("日志, 参数p1:"+p1+", 参数p2:"+ p2);
        //blablabla
    }
}

我个人觉得方便太多了。

按照原来的代码,客户说要增加日志,那我们肯定不愿意搞这些事情,但是现在只是加@WriteLog这样的方式的话,还是能接受的。

以下,我就将我的实现方法,公知于众,就算不能使用其中的代码,也可以从中得到一些启发吧。

首先声明一下,我是用java,基于spring框架——对于.net人员最多的博客园可能直接拷贝代码怕是不可能了。

第1步,声明@WriteLog(java中叫Annotation, .net里叫Attribute)

@Retention(RetentionPolicy.RUNTIME)
@Target(value={ElementType.METHOD})
public @interface WriteLog {
    public String value() default "";
    public WriteType type() default WriteType.after;

    public enum WriteType{
        before,
        after
    }
}

java中的Annotation和.net中的Attribute是有些不同的,java中的Annotation可以声明为SOURCE, CLASS, RUNTIME,表明Annotation的保存时效。这里需要声明为RUNTIME。

WriteType表明了是在什么时候记录日志,before,只能取到参数的信息,after还可以取到返回值是什么。

第2步,配置Spring Aop注入

在spring的主配置文件里,需要配置织入点(Pointcut)

<bean id="userLogPointcut" class="org.springframework.aop.support.annotation.AnnotationMatchingPointcut">
        <constructor-arg index="0" value="org.springframework.stereotype.Controller"></constructor-arg>
        <constructor-arg index="1" value="annotations.WriteLog"></constructor-arg>
    </bean>

这个 org.springframework.aop.support.annotation.AnnotationMatchingPointcut 是 Pointcut接口的一个实现。

什么是Pointcut呢,就是Spring做Aop时,需要在什么位置进行Aop。

那这个AnnotationMatchingPointcut 就是通过Annotation来进行查找哪些位置需要进行Pointcut。

第一个构造函数参数是表明有@Controller的类,第二个构造函数参数表明有@WriteLog的方法。

这只是配置了织入点,还需要告诉怎么处理这个织入点

    <bean id="userLogAdvice" class="WriteLogAdvice">
    </bean>

在java中有各种Advice(有before, after, methodInceptor),配置一个bean然后实现Advice,实现如下

public class WriteLogAdvice implements MethodInterceptor {
    private UserLogService service;    

    @Override
    public Object invoke(MethodInvocation arg0) throws Throwable {
        if(service==null){
            service = ContextUtil.getBean(UserLogService.class);
        }
        if(service != null){
            return service.insertUserLog(arg0);
        }
        return arg0.proceed();
    }

}

这里直接将方法交给service.insertUserLog中进行处理。

Spring接收Aop的bean是Advisor,所以还需要配置Advisor

    <bean id="userLogAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="advice" ref="userLogAdvice" />
        <property name="pointcut" ref="userLogPointcut" />
    </bean>

分别引用上面已经配置好的advice还pointcut。

第3步,进行日志记录的实现

在上一步中,我们将日志记录的实现交给service.insertUserLog来处理了。

这个日志记录实现,需要考虑的是参数信息如何填充到日志里,其它把日志放到哪(放到数据库,还是文件),都是简单的事情。

取值

我们可以从 MethodInvocation 对象中取到每个参数的值——但是不能取到参数名(到了运行时,都是什么 arg0, arg1之类无意义的参数名了)。

Object[] args = mi.getArguments();

这样就能取到全部参数的值对象了。

而使用

Object result = mi.proceed();

得到的就是方法的返回值对象。

填充

从“${p0.username}从${ctx.ip}登录, 登录${iif(ret.success,‘成功‘,‘失败‘)}” 这个字符串来看,很像jstl中的代码,其实正是使用jstl来实现的。

使用jstl的原因是

1. 这个代码就是放到web环境中运行的,那么jstl的jar包肯定会在其中

2. p0.username,调用的是p0.getUsername(),这个是jstl默认支持的

当然也可以使用其它模板引擎,如freemarker,或者自己实现一个也行——就是比较花时间。

我们用jstl都是在jsp中使用,那么在代码里怎么使用呢?这个我也在网上找了很久,都没有找到,最后只好反编译jstl的jar来查找一下。

下面是在java代码中使用jstl的关键代码

    /**
     * 实际执行日志的写入
     * @param mi 当前调用的函数
     * @param annotation WriteLog对象
     * @param result 函数返回值
     */
    private void realWriteLog(final MethodInvocation mi,
            final WriteLog annotation, final Object result) {
        try {
            if (annotation != null) {
                //声明一个VariableResolver 用于初始化 Evaluator
                MapVariableResolver vr = new MapVariableResolver(mi, result);
                //ELEvaluator 用来 evaluate
                ELEvaluator eval = new ELEvaluator(vr);
                //允许包含函数
                System.setProperty("javax.servlet.jsp.functions.allowed", "true");
                String msg = annotation.value();
                if(msg!=null){
                    //为了便书写,WriteLog中的单引号就表示双引号(不然还需要转义)
                    msg = msg.replaceAll("‘", "\"");
                    //执行evaluate,String.class表示eval返回的类型,fns是函数映射map,fn是函数前缀
                    Object obj = eval.evaluate(msg, null, String.class, fns, "fn");
                    //记录日志
                    userLog.info(obj);
                    //插入到数据库
                    dao.insert((String)obj);
                }
                vr.map.clear();
            }
        } catch (Exception ex) {
            log.warn("记录用户日志失败", ex);
        }
    }            

其中fns这个map对象,使用如下的方法得到

static Map<String, Method> fns = new HashMap<String, Method>();
    static{
        try {
            //此处添加jstl中的默认方法
            Method[] methods = Functions.class.getMethods();
            for(Method m : methods){
                if((m.getModifiers()&Modifier.STATIC)==Modifier.STATIC){
                    fns.put("fn:"+m.getName(), m);
                }
            }
            //还有一些自己定义的方法,也加入进去
            methods = WriteLogFunctions.class.getMethods();
            for(Method m : methods){
                if((m.getModifiers()&Modifier.STATIC)==Modifier.STATIC){
                    fns.put("fn:"+m.getName(), m);
                }
            }
        } catch (SecurityException e) {
            e.printStackTrace();
        }
    }

其中 MapVariableResolver 就是将方法的参数值放入到map中,要取的时候就从map中取出来。我这里是将该类的实现直接放到内部类来实现

private class MapVariableResolver implements VariableResolver{
        private Map<String, Object> map;
        public MapVariableResolver(MethodInvocation mi, Object result){
            Object[] args = mi.getArguments();
            map = new LinkedHashMap<String, Object>(args.length+2);
            for(int i=0; i<args.length; i++){
                map.put("p"+i, args[i]);
                if((!map.containsKey("ctx")) && args[i] instanceof HttpServletRequest){
                    map.put("ctx", new LogContextImpl((HttpServletRequest)args[i]));
                }
            }
            map.put("ret", result);
        }
        @Override
        public Object resolveVariable(String arg0, Object arg1)
                throws ELException {
            if(map.containsKey(arg0)){
                return map.get(arg0);
            }
            return "[no named("+arg0+") value]";
        }
    }
}

使用斜体的3行,增加了一个 LogContextImpl 对象,主要用于取HttpServletRequest对象里的session,以及ip。

private class LogContextImpl implements LogContext{
        private Map session;
        private String ip;
        public LogContextImpl(HttpServletRequest req){
            HttpSession session2 = req.getSession();
            if(session2 instanceof Map){
                session = (Map) session2;
            }else{
                session = new HashMap();
                Enumeration names = session2.getAttributeNames();
                while(names.hasMoreElements()){
                    String next = (String)names.nextElement();
                    session.put(next, session2.getAttribute(next));
                }
            }

            ip = req.getHeader("x-forwarded-for");
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = req.getHeader("Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = req.getHeader("WL-Proxy-Client-IP");
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = req.getRemoteAddr();
            }
        }
        public String getIp() {
            return ip;
        }

        public Map getSession(){
            return session;
        }
    }
public interface LogContext {
    @SuppressWarnings("rawtypes")
    public Map getSession();
    public String getIp();
}

至此,这个WriteLog的实现就全部完成了。

代码下载

在Spring中轻松写日志

时间: 2024-08-04 02:18:15

在Spring中轻松写日志的相关文章

Spring官方文档——日志

2.3.2 日志 日志对于Spring来说非常重要(废话,日志对哪个系统不重要?),因为 a)它是唯一强制的外部依赖,b)每个人都希望在使用某个工具时可以看到一些友好地输出,c)Spring集成了很多其他的工具,它们也都有自己的日志依赖.应用开发者的一个目标通常是:对于整个应用来说(包括所有的外部组件),集中创建一个统一的日志配置.由于现在有如此多的日志框架,这个选择看起来会变得更难. Logging is a very important dependency for Spring becau

Spring使用SLF4J代替Commons Logging写日志

项目的日志更换成slf4j和logback后,发现项目无法启动.错误提示java.lang.ClassNotFoundException: org.apache.commons.logging.Log,如图所示.原因是Spring默认使用commons logging写日志,需要桥接工具把日志输入重定向到slf4j.在项目中添加commons logging到slf4j的桥接器jcl-over-slf4j即可解决该问题. <dependency> <groupId>org.slf4

spring-logger spring中日志配置

默认日志 Logback:默认情况下,Spring Boot会用Logback来记录日志,并用INFO级别输出到控制台.在运行应用程序和其他例子时,你应该已经看到很多INFO级别的日志了. 从上图可以看到,日志输出内容元素具体如下: 时间日期:精确到毫秒日志级别:ERROR, WARN, INFO, DEBUG or TRACE进程ID分隔符:— 标识实际日志的开始线程名:方括号括起来(可能会截断控制台输出)Logger名:通常使用源代码的类名日志内容添加日志依赖假如maven依赖中添加了spr

Spring AOP 实现写事件日志功能

什么是AOP?AOP使用场景?AOP相关概念?Spring AOP组件?如何使用Spring AOP?等等这些问题请参考博文:Spring AOP 实现原理 下面重点介绍如何写事件日志功能,把日志保存到数据库中. 事件日志是与主业务功能无关的逻辑,用AOP实现是再好不过了,其中因为有些数据库日志表中的字段参数需要传递,所以会用到自定义注解,将这些参数用自定义注解传递过来. 1.自定义注解Operation package com.jykj.demo.filter; import java.lan

kettle作业(job)调用转换,设置变量,写日志到数据库中【转】

首先建立转换:从数据库表到日志 表输入的设置: 日志设置: 新建job: 转换选择刚才建好的输出日志转换.变量设置如下: 此ID就是转换中的${ID},执行job,可以看到控制台输出日志结果: 黑色字体部分中只写出了id=1的一条记录. 最后补充,将转换的日志写到数据库中:打开转换>ctrl+t>日志选项卡>转换>点击下面的SQL,执行SQL建表.执行完job会在数据库中写入日志记录.

轻松了解Spring中的控制反转和依赖注入(二)

紧接上一篇文章<轻松了解Spring中的控制反转和依赖注入>讲解了SpringIOC和DI的基本概念,这篇文章我们模拟一下SpringIOC的工作机制,使我们更加深刻的理解其中的工作. 类之间的结构图如下 以下是代码 BeanFactor接口:在Spring源码中的定义是:持有对一定数量的Bean的定义,同时每个Bean都被唯一标识的对象(类),需要实现这个接口.根据对Bean的定义,该工厂将会返回一个包含Bean定义的对象的独立实例(原型设计模式),或者单例共享(一个不错的单例设计模式,)范

轻松了解Spring中的控制反转和依赖注入

我们回顾一下计算机的发展史,从最初第一台计算机的占地面积达170平方米,重达30吨,到现如今的个人笔记本,事物更加轻量功能却更加丰富,这是事物发展过程中的一个趋势,在技术领域中同样也是如此,企业级JavaBean(Enterprise JavaBean ,EJB)在创建之初是非常成功,但是时间一久人们便开始追逐更加方便更加简易和轻量级的技术框架实现,于是Spring就应运而生,并且Sring一直开始不断地涉及到其他领域(这里就不再多详谈了),而Spring的精髓当中就包括控制反转和依赖注入. 浅

C# 简单的往txt中写日志,调试时很有用

原文 http://blog.csdn.net/hejialin666/article/details/6106648 有些程序在调试时很难抓住断点(如服务程序),有些程序需要循环无数次,要看每一次或某一次的结果,等等吧! 那就来个简单的写日志程序吧,txt文件生成在debug目录里 using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Windows.Fo

Airflow 中文文档:写日志

在本地编写日志 用户可以使用base_log_folder设置在airflow.cfg指定日志文件夹. 默认情况下,它位于AIRFLOW_HOME目录中. 此外,用户可以提供远程位置,以便在云存储中存储日志和日志备份. 在Airflow Web UI中,本地日志优先于远程日志. 如果找不到或访问本地日志,将显示远程日志. 请注意,只有在任务完成(包括失败)后才会将日志发送到远程存储. 换句话说,运行任务的远程日志不可用. 日志作为{dag_id}/{task_id}/{execution_dat