在项目开发中,由于时间问题,往往会采用边开发边研讨的策略,在不断打磨中找到最合适的解决方案。目前接手的工作是一部分接口开发,为了统一请求报文格式、记录请求日志、记录请求报文等,还要考虑已开发接口的改造成本,于是想到了利用Spring AOP机制,通过注解的方式进行切入,实现接口的统一接入管理。
业务场景:
在核心系统中往往存在一系列的外部接口,供其他渠道、平台进行访问。此时对于接口报文的格式、接口调用的日志记录等容易被忽视。
解决方案:
- 通过创建自定义注解,在需要被管理的接口方法上进行标记
- 编写AOP处理类,通过识别自定义注解作为切入点,然后利用一系列的切片捕捉进行逻辑处理
代码案例:
- 自定义注解:
package com.aikes.uniformAccess;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by aikes on 2020/4/28.
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UniformAccess {
String value() default "";
}
- Aspect处理类:
package com.aikes.uniformAccess;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.Map;
/**
* aikes 2020-04-28
* 接口统一接入代理类,用于接口验证、日志记录、报文存储
*
* 被管理的接口需要有如下配置:
* 1、使用@UniformAccess注解加在接受处理的方法上
* 2、接口入口方法参数必须是Map(方法内部可通过JSON中转为各自对象)
* 3、接口入口方法返回值必须是String(其他对象类型返回值通过JSON转为字符串即可)
*
*/
@Slf4j
@Component
@Aspect
public class InterfaceDelegate {
@Value("${filepath}")
public String mFilePath;
@Autowired
InterfaceDelegateService mInterfaceDelegateService;//用于日志记录、接口验证等操作
public static final String SUCCESS = "0";
public static final String FAIL = "-1";
@Pointcut("@annotation(com.aikes.uniformAccess.UniformAccess)")
public void uniformAccessPointcut() {
}
@SuppressWarnings("unchecked")
@Around(value = "uniformAccessPointcut()")
public String around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("------------------------------------接口访问开始!");
InterfaceRequest tInterfaceRequest = new InterfaceRequest();
Object[] obj = joinPoint.getArgs();
if (obj.length != 1 || !(obj[0] instanceof Map)) {
log.info("内部异常,该接口参数有误+"+joinPoint.getTarget());
return assembleErrorResponse("内部异常,该接口参数有误!", null);
}
//获取到方法的参数类型,用于后面参数转换(根据规范该类型始终为LinkedHashMap)
Class tClass = obj[0].getClass();
try {
//由于请求的参数已经经过spring的RequestBody处理变成了Map类型,此处需要将其Json化
String tJsonParam = JSON.toJSONString(obj[0]);
tInterfaceRequest = JSON.parseObject(tJsonParam, InterfaceRequest.class);
if(tInterfaceRequest.getTransNo() == null
|| tInterfaceRequest.getSystemId() == null
|| tInterfaceRequest.getBusinessType() == null
|| tInterfaceRequest.getServiceType() == null
|| tInterfaceRequest.getCreatetime() == null
|| tInterfaceRequest.getData() == null){
log.info("接口参数有误,请核实!");
return assembleErrorResponse("接口参数有误,请核实!", null);
}
} catch (Exception e) {
log.info("接口参数有误:" + e.getMessage());
return assembleErrorResponse("接口参数有误,请核实!", null);
}
//请求日志记录,请求报文保存
Map<String, Object> tMap = this.recordLog(tInterfaceRequest);
tMap.put("transNo", tInterfaceRequest.getTransNo());
if (tMap.get("error") != null) {
return assembleErrorResponse((String) tMap.get("error"), null);
}
log.info("日志ID:" + tMap.get("LogID"));
//判断是否有可用接口,若不存在程序阻断
if (!this.checkInterface(tInterfaceRequest)) {
log.info("该接口尚未配置!");
return assembleErrorResponse("该接口尚未配置!", tMap);
}
//拆包装,重新塞参数,组装响应数据
Object tResponseData = null;
try {
String tParamJson = JSON.toJSONString(tInterfaceRequest.getData());
//根据真实处理方法的参数类型进行数据封装
Object[] tParam = new Object[]{JSON.parseObject(tParamJson, tClass)};
tResponseData = joinPoint.proceed(tParam);
if (!(tResponseData instanceof String)) {
log.info("内部异常,该接口返回值有误:"+joinPoint.getTarget());
return assembleErrorResponse("内部异常,该接口返回值有误!", tMap);
}
} catch (Exception e) {
log.info("接口调用失败,内部处理异常:" + e.getMessage());
return assembleErrorResponse("接口调用失败:内部处理异常!", tMap);
}
//响应日志记录
tMap.put("response", tResponseData == null ? "":tResponseData);
this.updateLog(tMap, true);
log.info("------------------------------------接口访问结束!");
return tResponseData == null ? "":(String)tResponseData;
}
/**
* 日志记录,请求报文存储为文件
* @param cInterfaceRequest
* @return
*/
private Map<String, Object> recordLog(InterfaceRequest cInterfaceRequest) {
//请求报文保存
String tRequestFilePath = mFilePath + "interface/request/" + cInterfaceRequest.getTransNo() + ".json";
Json2Disk(tRequestFilePath, JSON.toJSONString(cInterfaceRequest));
//日志记录
return mInterfaceDelegateService.recordLog(cInterfaceRequest);
}
/**
* 日志更新、响应报文存储
* @param cMap
* @param cFlag
*/
private void updateLog(Map<String, Object> cMap, boolean cFlag) {
cMap.put("status", cFlag ? SUCCESS : FAIL);
String tResponseFilePath = mFilePath + "interface/response/" + cMap.get("transNo")+ ".json";
//响应报文保存
Json2Disk(tResponseFilePath, (String)cMap.get("response"));
cMap.put("responseFilePath",tResponseFilePath);
//日志记录更新
mInterfaceDelegateService.updateLog(cMap);
}
/**
* 校验该接口是否开放,即:数据库是否已配置该接口
* @param cInterfaceRequest
* @return
*/
private boolean checkInterface(InterfaceRequest cInterfaceRequest) {
//验证接口是否已在数据库配置
return true;
}
/**
* 接口访问异常返回
* @param cErrorMessage
* @param cMap 数据库日志数据未记录成功之前均可传Null , 旨在不进行日志更新操作
* @return
*/
@SuppressWarnings("unchecked")
private String assembleErrorResponse(String cErrorMessage, Map<String, Object> cMap) {
InterfaceResponse tInterfaceResponse = new InterfaceResponse();
tInterfaceResponse.setStatus(InterfaceResponse.FAIL);
tInterfaceResponse.setStatusText(InterfaceResponse.FAIL_TEXT);
tInterfaceResponse.setData(cErrorMessage);
if (cMap != null) {
cMap.put("response", JSON.toJSONString(tInterfaceResponse));
cMap.put("message", cErrorMessage);
this.updateLog(cMap, false);
}
return JSON.toJSONString(tInterfaceResponse);
}
/**
* 字符串存储为文件
* @param cFilePath
* @param cMessage
*/
private void Json2Disk(String cFilePath, String cMessage) {
PrintStream ps = null;
try {
File tFile = new File(cFilePath);
if (!tFile.exists()) {
tFile.getParentFile().mkdirs();
}
ps = new PrintStream(new FileOutputStream(tFile));
ps.println(cMessage);
} catch (FileNotFoundException e) {
log.info("报文写出失败!" + e.getMessage());
} finally {
if(ps!=null){
ps.close();
}
}
}
}
- 被切入目标方法
package com.aikes.api;
import com.alibaba.fastjson.JSON;
import com.aikes.configuration.ZuulUrlPortProperties;
import com.aikes.entity.HttpResult;
import com.aikes.uniformAccess.UniformAccess;
import com.aikes.utils.HttpClientUtil;
import com.aikes.utils.InterfaceRequest;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* aikes 连通性测试接口
*/
@RestController
@RequestMapping("aikes")
@Slf4j
@Api(value = "连通性测试接口", tags = "连通性测试接口")
public class AikesAPI {
@Autowired
private HttpClientUtil httpClientUtil;
@Autowired
private ZuulUrlPortProperties zuulUrlPortProperties;
@UniformAccess
@PostMapping("/testInterface")
public String testInterface(@RequestBody Map cMap) throws Exception {
String json = JSON.toJSONString(cMap);
//AikesBean cInterfaceRequest = JSON.parseObject(JSON.toJSONString(tMap), AikesBean.class);可以根据需要将Map对象数据拆解重组为对应Bean对象
log.info (LocalDateTime.now ().toString () + json);
HttpResult httpResult = httpClientUtil.doPostByJson (zuulUrlPortProperties.getZuulinsideurl()+ zuulUrlPortProperties.getZuulAikesServerurl()
+ "aikes/uniformAccess", json);
String result = httpResult.getMessage ();
log.info (LocalDateTime.now ().toString () + result);
return result;
}
}
- 实体类对象
“
package com.aikes.utils;
import lombok.Data;
import java.io.Serializable;
/**
* aikes
*/
@Data
public class InterfaceRequest<T> implements Serializable {
private static final long serialVersionUID = 1L;
//交易流水号
private String transNo;
//交易业务号
private String businessNo;
//用户信息认证
private String token;
//请求来源-系统ID
private String systemId;
//业务类型
private String businessType;
//服务类型
private String serviceType;
//创建日期
private String createtime;
//请求体
private T data;
}
package com.aikes.utils;
import ins.framework.lang.Springs;
import ins.framework.web.ApiResponse;
import ins.framework.web.exception.ExceptionHelper;
import java.io.Serializable;
/**
* aikes
*/
public class InterfaceResponse<T> implements Serializable {
private static final long serialVersionUID = 1L;
private long businessCode;
public static final int SUCCESS = 0;
public static final int FAIL = -1;
public static final int BUSY = -100;
public static final String SUCCESS_TEXT = "Success";
public static final String FAIL_TEXT = "Fail";
public static final String BUSY_TEXT = "Busy";
private long status;
private String statusText;
private T data;
//TODO 补充get/set、构造方法
}
注意事项:
-
使用AOP Around切入时,修改的参数类型必须和被切入的方法类型相同,否则在
tResponseData = joinPoint.proceed(tParam);
执行时会提示:无法将m类型转为n类型接收到的返回值
tResponseData
可以用Object暂时接纳,后续也只能强转为与被切入方法的返回值类型PS:本文案例统一使用Map为参数类型,String为返回值类型,大家可以根据需要自行调整
-
统一被切入方法入口参数后,对于后续操作不方便,可以使用JSON串中转为实际要处理的对象类型
AikesBean tAikesBean = JSON.parseObject(JSON.toJSONString(tMap), AikesBean.class);
-
案例中涉及到部分业务操作代码有部分删减,敬请理解。
版权声明:本文为AikesLs原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。