做了一个新项目,与周边系统交互比较多,随之而来的一个问题,就是当交互异常的时候不是很好分析具体原因。每次都需要去服务器拉去日志,然后分析。影响工作效率,因此考虑把部分关键接口的交易报文存储到一张表中,这样每次都可以直接去数据库查询。实现思路如下:
1.最简单的做法可能是在接口入口处获取下请求报文,请求时间等等,在出口处获取下响应报文,响应时间等等,然后保存下数据库。这样带来的一个弊端就是,记录日志与接口的业务逻辑耦合到了一起,影响代码的结构。而且维护不方便,假设有一百个接口需要加,难道要在100个地方写这种重复的代码?因此,针对这种系统共性的问题,在我理解来就属于一个切面。我们可以通过spring aop的思想来解决这个问题。
2.提到spring aop最开始想到的是使用spring框架中的interceptor,理论上讲使用interceptor是可以解决上面问题的。我们可以配置下需要拦截的接口,在preHandle和方法中分别得到请求报文和响应报文。本人最开始的时候也是采用的这种办法,在preHandle中获取请求报文是可以做到的,获取HttpServletRequest中的输入流,然后读取输入流即可获得请求报文。但是在获取响应报文的时候不好实现,因为HttpServletResponse中包含的是一个输出流。理论上讲输出流只能写,输入流只能读。因此我们是无法获取输出流中的内容的(可能也有办法获取,但是个人觉得不好实现,所以放弃。),所以此时就要考虑别的方式。
3.上面两种方式都有弊端,这时候要考虑别的实现方式,其实我们需要的就是监控一下目标接口,获取入参和出参。这种场景下,动态代理是最合适的。通过查找资料,找到了一种相对比较合理的解决方式。代码如下:
1)首先在pom文件中引入如下包:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.6.9</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.7.2</version>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.9.10</version>
</dependency>
2)定义日志对象
package cn.insurtech.claim.base.entity.tradelog;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "u_interface_trade_log", schema = "")
@SuppressWarnings("serial")
public class UInterfaceTradeLog {
@Id
private String id;
private String businessNo;
private String interfaceNo;
private String url;
private Integer timeSpan;
private String requestTime;
private String responseTime;
private String tradeStatus;
private String isServer;
private String method;
private String remoteAddr;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id == null ? null : id.trim();
}
public String getBusinessNo() {
return businessNo;
}
public void setBusinessNo(String businessNo) {
this.businessNo = businessNo == null ? null : businessNo.trim();
}
public String getInterfaceNo() {
return interfaceNo;
}
public void setInterfaceNo(String interfaceNo) {
this.interfaceNo = interfaceNo == null ? null : interfaceNo.trim();
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url == null ? null : url.trim();
}
public Integer getTimeSpan() {
return timeSpan;
}
public void setTimeSpan(Integer timeSpan) {
this.timeSpan = timeSpan;
}
public String getRequestTime() {
return requestTime;
}
public void setRequestTime(String requestTime) {
this.requestTime = requestTime == null ? null : requestTime.trim();
}
public String getResponseTime() {
return responseTime;
}
public void setResponseTime(String responseTime) {
this.responseTime = responseTime == null ? null : responseTime.trim();
}
public String getTradeStatus() {
return tradeStatus;
}
public void setTradeStatus(String tradeStatus) {
this.tradeStatus = tradeStatus == null ? null : tradeStatus.trim();
}
public String getIsServer() {
return isServer;
}
public void setIsServer(String isServer) {
this.isServer = isServer == null ? null : isServer.trim();
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method == null ? null : method.trim();
}
public String getRemoteAddr() {
return remoteAddr;
}
public void setRemoteAddr(String remoteAddr) {
this.remoteAddr = remoteAddr == null ? null : remoteAddr.trim();
}
private String requestBody;
private String responseBody;
private String exceptionStackTrace;
public String getRequestBody() {
return requestBody;
}
public void setRequestBody(String requestBody) {
this.requestBody = requestBody == null ? null : requestBody.trim();
}
public String getResponseBody() {
return responseBody;
}
public void setResponseBody(String responseBody) {
this.responseBody = responseBody == null ? null : responseBody.trim();
}
public String getExceptionStackTrace() {
return exceptionStackTrace;
}
public void setExceptionStackTrace(String exceptionStackTrace) {
this.exceptionStackTrace = exceptionStackTrace == null ? null : exceptionStackTrace.trim();
}
}
3)编写拦截器,拦截器是重点。这个类中用到了下面一些技术点:
自定义注解:自定义注解可以很方便的获取我们要拦截的接口的一些信息,并且可以在拦截的方法上面配置一些需要保存的属性,比如接口编号等。
局部线程变量:spring创建的拦截器是单例的,我们可能需要在拦截器中定义一个全局变量,供多个方法使用。考虑到并发情况,因此不能使用普通的局部变量,需要使用ThreadLocal来避免并发带来的问题。
反射:我们需要记录日志的时候保存当前请求报文中的业务号,可以配置下业务号所在的位置,通过反射获取。具体需要看下面代码:
package cn.insurtech.aop;
import cn.insurtech.bean.UInterfaceTradeLogWithBLOBs;
import cn.insurtech.dao.UInterfaceTradeLogMapper;
import com.alibaba.fastjson.JSON;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
/**
* Created by sxb-gt on 2017/12/30.
*/
@Aspect
@Component
public class TradeLogAspect {
Logger LOGGER = Logger.getLogger(TradeLog.class);
/** 建立日志对象线程变量 **/
private ThreadLocal<UInterfaceTradeLogWithBLOBs> threadLocal = new ThreadLocal<UInterfaceTradeLogWithBLOBs>();
@Autowired
private UInterfaceTradeLogMapper uInterfaceTradeLogMapper;
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
private final static Map<String, TradeLogProperties> TRADE_LOG_PROPERTIES_MAP = TradeLogAnnotationParse.getUriTradelogMap();
@Before("@annotation(TradeLog)")
public void logBefore(JoinPoint point) {
try{
/** 获取请求对象 **/
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes)ra;
HttpServletRequest request = sra.getRequest();
String uri = request.getRequestURI();
String url = request.getRequestURL().toString();
TradeLogProperties tradeLogProperties = TRADE_LOG_PROPERTIES_MAP.get(uri);
UInterfaceTradeLogWithBLOBs uInterfaceTradeLog = new UInterfaceTradeLogWithBLOBs();
/** 设置UUID为主键 **/
uInterfaceTradeLog.setId(UUID.randomUUID().toString().replace("-", ""));
/** 请求路径 **/
uInterfaceTradeLog.setUrl(url);
/** 业务号 **/
uInterfaceTradeLog.setBusinessNo(getBusinessNo(tradeLogProperties,point.getArgs()[0]));
/** 接口编号 **/
uInterfaceTradeLog.setInterfaceNo(tradeLogProperties.getInterfaceNo());
/** 请求时间 **/
uInterfaceTradeLog.setRequestTime(sdf.format(new Date()));
/** 请求报文 **/
if (point.getArgs() != null && point.getArgs().length > 0) {
Object parameterObject = point.getArgs()[0];
if (parameterObject instanceof String) {
uInterfaceTradeLog.setRequestBody((String) parameterObject);
}else {
uInterfaceTradeLog.setRequestBody(JSON.toJSONString(parameterObject));
}
}
/** 请求对应的controller及method名称 **/
uInterfaceTradeLog.setMethod(point.getSignature().getDeclaringTypeName() + "." + point.getSignature().getName());
/** 存储用户的IP **/
uInterfaceTradeLog.setRemoteAddr(getIpAddress(request));
/** “是否当前系统为服务方” **/
uInterfaceTradeLog.setIsServer("1");
threadLocal.set(uInterfaceTradeLog);
}catch (Exception e) {
LOGGER.error("日志记录报错!");
e.printStackTrace();
}
}
/**
* 此处execution表达式,拦截所有的方法。暂时没找到合适的表达式拦截多个制定的方法。
*/
@Pointcut("@annotation(TradeLog)")
public void resultMapAspect() {
}
@AfterReturning(value = "resultMapAspect()", returning = "resultMap")
public void logAfterReturning(JoinPoint joinpoint, Object resultMap) throws Throwable {
try{
/** 从当前线程中取出日志记录对象 **/
UInterfaceTradeLogWithBLOBs uInterfaceTradeLog = threadLocal.get();
/** 如果uInterfaceTradeLog为null说明该方法不需要拦截 **/
if (uInterfaceTradeLog == null) {
return;
}
Date responseTime = new Date();
/** 响应时间 **/
uInterfaceTradeLog.setResponseTime(sdf.format(responseTime));
/** 响应报文 **/
if (resultMap instanceof String) {
uInterfaceTradeLog.setResponseBody((String)resultMap);
}else {
uInterfaceTradeLog.setResponseBody(JSON.toJSONString(resultMap));
}
/** 交互状态,有返回代表交互成功,状态为1。抛异常代表交互失败,状态为0 **/
uInterfaceTradeLog.setTradeStatus("1");
/** 接口交互消耗时长 **/
uInterfaceTradeLog.setTimeSpan((int)(responseTime.getTime()- sdf.parse(uInterfaceTradeLog.getRequestTime()).getTime()));
System.out.println("插入开始:" + System.currentTimeMillis());
uInterfaceTradeLogMapper.insertSelective(uInterfaceTradeLog);
System.out.println("插入结束:" + System.currentTimeMillis());
}catch (Exception e) {
LOGGER.error("日志记录报错!");
e.printStackTrace();
}
}
@AfterThrowing(throwing="ex"
, pointcut=("@annotation(TradeLog)"))
// 声明ex时指定的类型会限制目标方法必须抛出指定类型的异常
// 此处将ex的类型声明为Throwable,意味着对目标方法抛出的异常不加限制
public void logException(Throwable ex)
{
try {
/** 从当前线程中取出日志记录对象 **/
UInterfaceTradeLogWithBLOBs uInterfaceTradeLog = threadLocal.get();
/** 如果uInterfaceTradeLog为null说明该方法不需要拦截 **/
if (uInterfaceTradeLog == null) {
return;
}
Date responseTime = new Date();
/** 响应时间 **/
uInterfaceTradeLog.setResponseTime(sdf.format(responseTime));
/** 响应报文 **/
uInterfaceTradeLog.setResponseBody(ex.toString());
/** 交互状态,有返回代表交互成功,状态为1。抛异常代表交互失败,状态为0 **/
uInterfaceTradeLog.setTradeStatus("0");
/** 接口交互消耗时长 **/
try {
uInterfaceTradeLog.setTimeSpan((int)(responseTime.getTime()- sdf.parse(uInterfaceTradeLog.getRequestTime()).getTime()));
} catch (ParseException e) {
uInterfaceTradeLog.setTimeSpan(999999);
}
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
ex.printStackTrace(printWriter);
StringBuffer exceptionStackTrace = stringWriter.getBuffer();
/** 异常堆栈 **/
uInterfaceTradeLog.setExceptionStackTrace(exceptionStackTrace.toString());
System.out.println("插入开始:" + System.currentTimeMillis());
uInterfaceTradeLogMapper.insertSelective(uInterfaceTradeLog);
System.out.println("插入结束:" + System.currentTimeMillis());
}catch (Exception e) {
LOGGER.error("日志记录报错!");
e.printStackTrace();
}
}
/**
* 获取用户真实IP地址,不使用request.getRemoteAddr();的原因是有可能用户使用了代理软件方式避免真实IP地址,
* 参考文章: http://developer.51cto.com/art/201111/305181.htm
*
* 可是,如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值,究竟哪个才是真正的用户端的真实IP呢?
* 答案是取X-Forwarded-For中第一个非unknown的有效IP字符串。
*
* 如:X-Forwarded-For:192.168.1.110, 192.168.1.120, 192.168.1.130,
* 192.168.1.100
*
* 用户真实IP为: 192.168.1.110
*
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
/**
* 通过反射获取业务单号
* @param tradeLogProperties 接口日志记录相关属性对象
* @param inputParameter controller入参
* @return
*/
private String getBusinessNo(TradeLogProperties tradeLogProperties,Object inputParameter){
String businessNo = "";
/** 获取businessno 所在位置 **/
String[] filedArray = tradeLogProperties.getBusinessNo().split("\\.");
/** controller的入参要么是string,要么是json对象 **/
if (inputParameter instanceof String) {
/** 入参对象对应的JSON对象CLASS类型 **/
String className = tradeLogProperties.getParameterClass();
/** businessno为空代表入参即为业务号**/
if ("".equals(tradeLogProperties.getBusinessNo())) {
return (String) inputParameter;
}else {
try {
inputParameter = JSON.parseObject((String)inputParameter,Class.forName(className));
}catch (Exception e) {
e.printStackTrace();
}
}
}
for (String filed : filedArray) {
try {
Method method = inputParameter.getClass().getDeclaredMethod("get" + filed,null);
inputParameter = method.invoke(inputParameter,null);
} catch (Exception e) {
e.printStackTrace();
}
}
if (inputParameter instanceof String) {
businessNo = (String ) inputParameter;
}
return businessNo;
}
}
上面这个类,只是提供了一个思路。针对具体的项目,还是需要调整的。
4)自定义注解,自定义注解有两个好处:第一,是可以取代配置文件。最开始的时候我考虑的也是写一个配置文件,通过读取配置文件的方式来获取需要拦截的方法,但是感觉配置文件比较麻烦,没有注解方便。 第二,是方便上面的拦截器拦截,如果不通过拦截注解的方式,我们每次写完配置文件,还需要编写一个execution拦截表达式,比较繁琐。
package cn.insurtech.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by sxb-gt on 2018/1/3.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface TradeLog {
/** 接口编号,不能重复 **/
String interfaceno();
/** 业务号位置 **/
String businessno()default "";
/** 入参对象类型 **/
String parameterclass() default "";
}
5)在需要拦截的controller上面配置自定义注解
@RequestMapping(value = "/sendClaimInfo", method = RequestMethod.POST)
@TradeLog(interfaceno = "CL001",parameterclass = "cn.insurtech.externalentity.liveneo.cld01.SendClaimInfoRequest",businessno = "BusinessBody.SendClaimInfo.RegistNo")
public String sendClaimInfo(@RequestBody String jsonDate) {
/** 业务逻辑部分 **/
}
6)自定义注解扫描,这个类是在系统启动的时候扫描指定目录下所有配置自定义注解的方法。
package cn.insurtech.aop;
import org.apache.commons.lang.StringUtils;
import org.reflections.Reflections;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Created by sxb-gt on 2018/1/3.
*/
@Component
//@PropertyScource("application.properties")
public class TradeLogAnnotationParse {
/** uri与日志记录配置对象映射map **/
private final static Map<String, TradeLogProperties> URI_TRADELOG_MAP = new HashMap<String, TradeLogProperties>();
private static boolean IS_INIT = false;
private static String basePackage;
private static String projectName;
@Value("${tradelog.basepackage}")
public void setBasePackage(String basePackage) {
TradeLogAnnotationParse.basePackage = basePackage;
}
@Value("${tradelog.projectname}")
public void setProjectName(String projectName) {
TradeLogAnnotationParse.projectName = projectName;
}
/** 加载配置文件 **/
private static void init() {
try {
Reflections reflections = new Reflections(basePackage);
Set<Class<?>> classesList = reflections.getTypesAnnotatedWith(RestController.class);
classesList.addAll(reflections.getTypesAnnotatedWith(Controller.class));
for (Class classes : classesList) {
//得到该类下面的所有方法
Method[] methods = classes.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(TradeLog.class)) {
//得到该类下面的RequestMapping注解
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
TradeLog tradeLog = method.getAnnotation(TradeLog.class);
String uri = projectName + requestMapping.value()[0];
Class parameterClass = method.getParameterTypes()[0];
String interfaceno = tradeLog.interfaceno();
String businessno = tradeLog.businessno();
TradeLogProperties tradeLogProperties = new TradeLogProperties();
tradeLogProperties.setUri(uri);
if (StringUtils.isBlank(tradeLog.parameterclass())) {
tradeLogProperties.setParameterClass(parameterClass.getName());
}else {
tradeLogProperties.setParameterClass(tradeLog.parameterclass());
}
tradeLogProperties.setInterfaceNo(interfaceno);
tradeLogProperties.setBusinessNo(businessno);
System.out.println("url=============" + uri);
System.out.println("parameterclass=="+parameterClass.getName());
System.out.println("interfaceno====="+interfaceno);
System.out.println("businessno====="+businessno);
URI_TRADELOG_MAP.put(uri,tradeLogProperties);
}
}
}
}catch (Exception e) {
e.printStackTrace();
}
}
public static Map<String, TradeLogProperties> getUriTradelogMap() {
if (!IS_INIT) {
init();
}
return URI_TRADELOG_MAP;
}
}
以上只是部分关键代码,主要是提供一个思路。通过本次实现这个功能,对AOP,自定义注解还是有了更多一点的认识,记录一下方便以后再需要用到的时候查阅。