IT源码网

简单日志集成

xmjava 2021年10月13日 编程语言 460 0

文章出现在个人博客首页时,博客园Markdown支持有问题,请点击标题再阅读,避免展示错误

我们在开发应用时,为了便于调试和业务需要,在业务逻辑中加入了许多日志,通常这样会给人一种感觉:业务和日志耦合了,因此我们很自然的剔除掉了许多日志,并采用AOP实现日志切面。
然而在许多情况下,我们并不能保证每个程序开发人员的代码都能做到整齐划一,逻辑复杂程度类似,某些和业务逻辑代码紧密关联的日志是无可避免的,毕竟太理想化的场景对业务和程序开发本身要求都比较高。

本文对这两种情形的极端情况都提供了处理策略.

1,完全的AOP切面提供日志,业务逻辑不存在任何日志代码
2,扩展log4j Appender,所有日志都分布在业务逻辑代码当中

为了方便日志接入到第三方系统,本文采用Activemq消息服务器接收日志.

AOP日志集成#

AOP切面方式实在是太热门,这种方式的优点是无侵入,下文将通过简单的业务场景,展示这种实践过程。

温馨提示:请先行启动外置的Activemq(从官网下载安装包,默认条件启动即可),如果未启动,请打开Spring配置文件中内置broker服务

业务流##

执行业务方法getCustomer ---> 被日志切面拦截:执行正常业务处理pjp.proceed(),然后发送日志给目标队列(demo.business.log) ---> 监听器(demo.business.log)收到消息

样例代码##

业务服务

package org.wit.ff.business; 
 
import org.springframework.stereotype.Service; 
import org.wit.ff.model.Customer; 
 
/** 
 * Created by F.Fang on 2015/11/10. 
 * Version :2015/11/10 
 */ 
 
@Service 
public class CustomerBusiness { 
 
    public Customer getCustomer(int appId, int customerId){ 
        Customer customer = new Customer(); 
        customer.setCompanyId(10010); 
        customer.setId(customerId); 
        customer.setTitle("hnb"); 
        customer.setName("cxb"); 
        customer.setLevel(Integer.MAX_VALUE); 
        return new Customer(); 
    } 
 
    public void saveCustomer(int appId, Customer customer){ 
        System.out.println("appId is:"+appId); 
        System.out.println("customer is:"+customer); 
    } 
 
} 

备注:无任何Log4j日志代码

模型

package org.wit.ff.model; 
 
/** 
 * Created by F.Fang on 2015/11/10. 
 * Version :2015/11/10 
 */ 
public class Customer { 
 
    private int id; 
 
    private int companyId; 
 
    private String name; 
 
    private int level; 
 
    private String title; 
 
    public int getCompanyId() { 
        return companyId; 
    } 
 
    public void setCompanyId(int companyId) { 
        this.companyId = companyId; 
    } 
 
    public String getTitle() { 
        return title; 
    } 
 
    public void setTitle(String title) { 
        this.title = title; 
    } 
 
    public int getLevel() { 
        return level; 
    } 
 
    public void setLevel(int level) { 
        this.level = level; 
    } 
 
    public String getName() { 
        return name; 
    } 
 
    public void setName(String name) { 
        this.name = name; 
    } 
 
    public int getId() { 
        return id; 
    } 
 
    public void setId(int id) { 
        this.id = id; 
    } 
} 
 

日志切面

package org.wit.ff.log; 
 
import org.aspectj.lang.ProceedingJoinPoint; 
import org.aspectj.lang.annotation.Around; 
import org.aspectj.lang.annotation.Aspect; 
import org.springframework.beans.factory.annotation.Autowired; 
import org.springframework.jms.core.JmsTemplate; 
import org.springframework.jms.core.MessageCreator; 
import org.wit.ff.util.JsonUtil; 
 
import javax.jms.Destination; 
import javax.jms.JMSException; 
import javax.jms.Message; 
import javax.jms.Session; 
import java.io.PrintWriter; 
import java.io.StringWriter; 
 
/** 
 * Created by F.Fang on 2015/11/10. 
 * Version :2015/11/10 
 */ 
@Aspect 
public class BusinessLogAspect { 
 
    @Autowired 
    private JmsTemplate jmsTemplate; 
 
    @Autowired 
    private Destination destination; 
 
    @Around("execution(* org.wit.ff.business.*.*(..))") 
    public Object record(ProceedingJoinPoint pjp) throws Throwable { 
        try { 
            Object result = pjp.proceed(); 
            // 添加正常处理的日志. 
            sendMsg(buildLog(pjp, null)); 
            return result; 
        } catch (Throwable e) { 
            // 增加异常处理的日志. 
            sendMsg(buildLog(pjp, e)); 
            throw e; 
        } 
    } 
 
    private TraceLog buildLog(ProceedingJoinPoint pjp, Throwable e) { 
        TraceLog log = new TraceLog(); 
        // 要保证所有的逻辑方法在调用参数上做限定,必须保证第一个参数是appId. 
        if (pjp.getArgs() != null && pjp.getArgs().length >= 1) { 
            log.setAppId((int) pjp.getArgs()[0]); 
        } 
        log.setOperation(pjp.getSignature().getName()); 
        if (null != e) { 
            String msg = getStackTrace(e); 
            if(msg.length()>256){ 
                log.setDetails(msg.substring(0,256)); 
            } else{ 
                log.setDetails(msg); 
            } 
        } 
        return log; 
    } 
 
    private void sendMsg(final TraceLog log) { 
        jmsTemplate.send(destination, new MessageCreator() { 
            @Override 
            public Message createMessage(Session paramSession) throws JMSException { 
                return paramSession.createTextMessage(JsonUtil.objectToJson(log)); 
            } 
        }); 
    } 
 
    /** 
     * 获取目标异常栈信息. 
     * 由于异常栈信息可能过长,如果考虑将数据入库或其它介质,最好考虑最大长度不超过一个阀值. 
     * 
     * @param throwable 目标异常. 
     * @return 
     */ 
    private String getStackTrace(Throwable throwable) { 
        StringWriter sw = new StringWriter(); 
        PrintWriter pw = new PrintWriter(sw); 
        try { 
            throwable.printStackTrace(pw); 
            return sw.toString(); 
        } finally { 
            pw.close(); 
        } 
    } 
 
} 
 

日志模型

package org.wit.ff.log; 
 
import org.apache.commons.lang3.builder.ReflectionToStringBuilder; 
import org.apache.commons.lang3.builder.ToStringStyle; 
 
/** 
 * Created by F.Fang on 2015/11/10. 
 * Version :2015/11/10 
 */ 
public class TraceLog { 
 
    private String operation; 
 
    /** 
     * 任何一个操作都需要一个应用,此属性用于标识不同的应用数据接入. 
     */ 
    private int appId; 
 
    /** 
     * 详细信息. 
     */ 
    private String details; 
 
    public String getOperation() { 
        return operation; 
    } 
 
    public void setOperation(String operation) { 
        this.operation = operation; 
    } 
 
    public int getAppId() { 
        return appId; 
    } 
 
    public void setAppId(int appId) { 
        this.appId = appId; 
    } 
 
    public String getDetails() { 
        return details; 
    } 
 
    public void setDetails(String details) { 
        this.details = details; 
    } 
 
    @Override 
    public String toString() { 
        return ReflectionToStringBuilder.toString(this, ToStringStyle.DEFAULT_STYLE); 
    } 
} 
 

消息监听

package org.wit.ff.log; 
 
import org.slf4j.Logger; 
import org.slf4j.LoggerFactory; 
import org.wit.ff.util.JsonUtil; 
 
import javax.jms.JMSException; 
import javax.jms.Message; 
import javax.jms.MessageListener; 
import javax.jms.TextMessage; 
 
/** 
 * Created by F.Fang on 2015/11/10. 
 * Version :2015/11/10 
 */ 
public class BusinessLogMessageListener implements MessageListener{ 
 
    private static final Logger LOGGER = LoggerFactory.getLogger(BusinessLogAspect.class); 
 
    @Override 
    public void onMessage(Message message) { 
        // 处理消息. 
        TextMessage txtMsg = (TextMessage) message; 
        try { 
            TraceLog log = JsonUtil.jsonToObject(txtMsg.getText(), TraceLog.class); 
            LOGGER.info("business log:"+log.toString()); 
        } catch (JMSException e) { 
            LOGGER.error("处理业务日志发生异常!", e); 
        } 
    } 
} 

日志配置

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> 
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> 
    <!-- ===================================================================== --> 
    <!--  以下是appender的定义                                                 --> 
    <!-- ===================================================================== --> 
 
    <!-- org.apache.log4j.ConsoleAppender --> 
    <appender name="PROJECT-CONSOLE" class="org.apache.log4j.ConsoleAppender"> 
        <layout class="org.apache.log4j.PatternLayout"> 
            <param name="ConversionPattern" value="%d %-5p %c{2} - %m%n"/> 
        </layout> 
    </appender> 
    
    <appender name="businessAppender" class="org.apache.log4j.DailyRollingFileAppender"> 
        <param name="file" value="logs/business.log"/> 
        <!-- 若配置为true,表示在原有日志上继续append --> 
        <param name="append" value="true"/> 
        <!-- 若配置为false,表示清空原有日志 --> 
        <!-- <param name="append" value="false"/> --> 
        <param name="encoding" value="UTF-8"/> 
        <layout class="org.apache.log4j.PatternLayout"> 
            <param name="ConversionPattern" value="%d %-5p %c{2} - %m%n"/> 
        </layout> 
    </appender> 
     
    <!-- 定义logger,链接多个Appender表示信息将输出到多个目标(可以是文件,也可以是控制台或其它) --> 
    <logger name="org.wit.ff.business" additivity="false"> 
        <level value="INFO"/> 
        <appender-ref ref="businessAppender"/> 
        <appender-ref ref="PROJECT-CONSOLE"/> 
    </logger> 
     
    <!-- ===================================================================== --> 
    <!--  Root logger的定义                                                    --> 
    <!-- ===================================================================== --> 
    <root> 
    <!--  DEBUG < INFO < WARN < ERROR < FATAL --> 
        <level value="INFO"></level> 
        <!-- <level value="WARN"/> --> 
        <appender-ref ref="PROJECT-CONSOLE"/> 
    </root> 
</log4j:configuration> 

Spring配置文件(spring-log-aop.xml)

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"  xmlns:aop="http://www.springframework.org/schema/aop" 
	xsi:schemaLocation=" 
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
     http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd 
     http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> 
 
 
    <!-- 本地内置的代理服务, 如果外置的Activemq已启动,请注释 --> 
    <!-- 
    <bean id="localBroker" class="org.apache.activemq.broker.BrokerService" 
          init-method="start" destroy-method="stop"> 
        <property name="brokerName" value="mainBroker" /> 
        <property name="persistent" value="false" /> 
        <property name="transportConnectorURIs"> 
            <list> 
                <value>tcp://localhost:61616</value> 
            </list> 
        </property> 
    </bean> 
    --> 
 
    <!-- 客户端连接工厂 --> 
    <bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"> 
        <property name="brokerURL"> 
            <value>tcp://localhost:61616</value> 
        </property> 
    </bean> 
 
    <!-- Jms模版 --> 
    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"> 
        <property name="connectionFactory" ref="connectionFactory" /> 
    </bean> 
 
    <!-- 目标队列 --> 
    <bean id="destination" class="org.apache.activemq.command.ActiveMQQueue"> 
        <constructor-arg value="demo.business.log" /> 
    </bean> 
 
    <!-- 监听器. --> 
    <bean id="businessLogListenerContainer" 
          class="org.springframework.jms.listener.DefaultMessageListenerContainer"> 
        <property name="connectionFactory" ref="connectionFactory" /> 
        <property name="destinationName" value="demo.business.log" /> 
        <property name="messageListener" ref="messageListener" /> 
    </bean> 
 
    <bean id="messageListener" class="org.wit.ff.log.BusinessLogMessageListener" /> 
 
    <!-- 日志Aspect扫描 --> 
    <bean id="logAspect" class="org.wit.ff.log.BusinessLogAspect" /> 
 
 
    <aop:aspectj-autoproxy proxy-target-class="true"/> 
 
    <!-- 启动service扫描 --> 
	<context:component-scan base-package="org.wit.ff.business"/> 
</beans> 
 

测试

package org.wit.ff.business; 
 
import org.junit.Test; 
import org.junit.runner.RunWith; 
import org.springframework.beans.factory.annotation.Autowired; 
import org.springframework.test.context.ContextConfiguration; 
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; 
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 
import org.wit.ff.model.Customer; 
 
/** 
 * Created by F.Fang on 2015/11/10. 
 * Version :2015/11/10 
 */ 
 
@ContextConfiguration(locations = "classpath:spring-log-aop.xml") 
@RunWith(SpringJUnit4ClassRunner.class) 
public class BusinessLogTest extends AbstractJUnit4SpringContextTests{ 
 
    @Autowired 
    private CustomerBusiness customerBusiness; 
 
    @Test 
    public void demo() throws Exception { 
        customerBusiness.getCustomer(1,1); 
        Thread.sleep(10000); 
    } 
 
} 
 

控制台日志

2015-11-11 00:58:19,672 INFO  log.BusinessLogAspect - business log:org.wit.ff.log.TraceLog@191a9961[operation=getCustomer,appId=1,details=<null>] 

扩展log4j appender#

扩展log4j appender是非常廉价的,自定义一个Appender即可,log4j的体系结构中,appender对应了一个目标输出介质,可以是文件、控制台、数据库。

业务流##

每一条日志都导向了CommonLogAppender ---> CommonBusiness执行getCustomer()方法,内部执行LOGGER.info(xxx)方法,实际日志内容是getCutomer,appId=1 --> CommonLogAppender执行append方法,并发送日志到队列(demo.common.log) ---> 监听器接收日志并打印到控制台。

样例代码##

自定义Appender

package org.wit.ff.log; 
 
import org.apache.activemq.ActiveMQConnectionFactory; 
import org.apache.activemq.pool.PooledConnectionFactory; 
import org.apache.log4j.AppenderSkeleton; 
import org.apache.log4j.spi.LoggingEvent; 
 
import javax.jms.*; 
 
/** 
 * Created by F.Fang on 2015/11/10. 
 * Version :2015/11/10 
 */ 
public class CommonLogAppender extends AppenderSkeleton { 
 
    private static final String COMMON_LOG_QUEUE = "demo.common.log"; 
    private PooledConnectionFactory pooledConnectionFactory; 
 
    @Override 
    public void activateOptions() { 
        ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(); 
        connectionFactory.setBrokerURL("tcp://localhost:61616"); 
        pooledConnectionFactory = new PooledConnectionFactory(connectionFactory); 
        pooledConnectionFactory.setMaxConnections(1); 
        pooledConnectionFactory.setMaximumActiveSessionPerConnection(2); 
    } 
 
    @Override 
    protected void append(LoggingEvent event) { 
        Connection connection = null; 
        Session session = null; 
        try { 
            connection = pooledConnectionFactory.createConnection(); 
            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); 
            MessageProducer producer = session.createProducer(session.createQueue(COMMON_LOG_QUEUE)); 
            if(event.getMessage()!=null){ 
                TextMessage txtMsg = session.createTextMessage(event.getMessage().toString()); 
                producer.send(txtMsg); 
            } 
        } catch (JMSException e) { 
            e.printStackTrace(); 
        } finally { 
            if (session != null) { 
                try { 
                    session.close(); 
                } catch (JMSException e) { 
                    e.printStackTrace(); 
                } 
            } 
            if (connection != null) { 
                try { 
                    connection.close(); 
                } catch (JMSException e) { 
                    e.printStackTrace(); 
                } 
            } 
        } 
    } 
 
    @Override 
    public void close() { 
        System.out.println("close!!!"); 
        pooledConnectionFactory.stop(); 
    } 
 
    @Override 
    public boolean requiresLayout() { 
        return true; 
    } 
} 
 

业务服务

package org.wit.ff.business; 
 
import org.slf4j.Logger; 
import org.slf4j.LoggerFactory; 
import org.springframework.stereotype.Service; 
import org.wit.ff.model.Customer; 
 
/** 
 * Created by F.Fang on 2015/11/10. 
 * Version :2015/11/10 
 */ 
@Service 
public class CommonBusiness { 
 
    private static final Logger LOGGER = LoggerFactory.getLogger(CommonBusiness.class); 
 
    public Customer getCustomer(int appId, int customerId){ 
        LOGGER.info("getCustomer, appId="+appId); 
        Customer customer = new Customer(); 
        customer.setCompanyId(10010); 
        customer.setId(customerId); 
        customer.setTitle("hnb"); 
        customer.setName("cxb"); 
        customer.setLevel(Integer.MAX_VALUE); 
        return new Customer(); 
    } 
 
    public void saveCustomer(int appId, Customer customer){ 
        LOGGER.info("saveCustomer, appId="+appId); 
        System.out.println("appId is:"+appId); 
        System.out.println("customer is:"+customer); 
    } 
 
} 
 

log4j配置

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> 
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> 
    <!-- ===================================================================== --> 
    <!--  以下是appender的定义                                                 --> 
    <!-- ===================================================================== --> 
 
    <!-- org.apache.log4j.ConsoleAppender --> 
    <appender name="PROJECT-CONSOLE" class="org.wit.ff.log.CommonLogAppender"> 
        <layout class="org.apache.log4j.PatternLayout"> 
            <param name="ConversionPattern" value="%d %-5p %c{2} - %m%n"/> 
        </layout> 
    </appender> 
    
    <appender name="businessAppender" class="org.apache.log4j.DailyRollingFileAppender"> 
        <param name="file" value="logs/business.log"/> 
        <!-- 若配置为true,表示在原有日志上继续append --> 
        <param name="append" value="true"/> 
        <!-- 若配置为false,表示清空原有日志 --> 
        <!-- <param name="append" value="false"/> --> 
        <param name="encoding" value="UTF-8"/> 
        <layout class="org.apache.log4j.PatternLayout"> 
            <param name="ConversionPattern" value="%d %-5p %c{2} - %m%n"/> 
        </layout> 
    </appender> 
     
    <!-- 定义logger,链接多个Appender表示信息将输出到多个目标(可以是文件,也可以是控制台或其它) --> 
    <logger name="org.wit.ff.business" additivity="false"> 
        <level value="INFO"/> 
        <appender-ref ref="businessAppender"/> 
        <appender-ref ref="PROJECT-CONSOLE"/> 
    </logger> 
     
    <!-- ===================================================================== --> 
    <!--  Root logger的定义                                                    --> 
    <!-- ===================================================================== --> 
    <root> 
    <!--  DEBUG < INFO < WARN < ERROR < FATAL --> 
        <level value="INFO"></level> 
        <!-- <level value="WARN"/> --> 
        <appender-ref ref="PROJECT-CONSOLE"/> 
    </root> 
</log4j:configuration> 

Spring配置

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"  xmlns:aop="http://www.springframework.org/schema/aop" 
	xsi:schemaLocation=" 
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
     http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd 
     http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> 
 
 
    <!-- 本地内置的代理服务 --> 
    <!-- 
    <bean id="localBroker" class="org.apache.activemq.broker.BrokerService" 
          init-method="start" destroy-method="stop"> 
        <property name="brokerName" value="mainBroker" /> 
        <property name="persistent" value="false" /> 
        <property name="transportConnectorURIs"> 
            <list> 
                <value>tcp://localhost:61616</value> 
            </list> 
        </property> 
    </bean> 
    --> 
 
    <!-- 客户端连接工厂 --> 
    <bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"> 
        <property name="brokerURL"> 
            <value>tcp://localhost:61616</value> 
        </property> 
    </bean> 
 
    <!-- 监听器. --> 
    <bean id="businessLogListenerContainer" 
          class="org.springframework.jms.listener.DefaultMessageListenerContainer"> 
        <property name="connectionFactory" ref="connectionFactory" /> 
        <property name="destinationName" value="demo.common.log" /> 
        <property name="messageListener" ref="messageListener" /> 
    </bean> 
 
    <bean id="messageListener" class="org.wit.ff.log.CommonLogMessageListener" /> 
 
    <!-- 启动service扫描 --> 
	<context:component-scan base-package="org.wit.ff.business"/> 
</beans> 

测试

package org.wit.ff.business; 
 
import org.junit.Test; 
import org.junit.runner.RunWith; 
import org.springframework.beans.factory.annotation.Autowired; 
import org.springframework.test.context.ContextConfiguration; 
import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; 
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 
 
/** 
 * Created by F.Fang on 2015/11/10. 
 * Version :2015/11/10 
 */ 
@ContextConfiguration(locations = "classpath:spring-log-expand-log4j.xml") 
@RunWith(SpringJUnit4ClassRunner.class) 
public class CommonLogTest extends AbstractJUnit4SpringContextTests { 
 
    @Autowired 
    private CommonBusiness commonBusiness; 
 
    @Test 
    public void demo() throws Exception { 
        commonBusiness.getCustomer(1, 1); 
        Thread.sleep(10000); 
    } 
} 

日志记录

getCustomer, appId=1 

QA#


评论关闭
IT源码网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!

轻量级封装DbUtils&Mybatis之一概要