简单日志集成
文章出现在个人博客首页时,博客园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