利用AOP进行接口的统计接入管理

  • Post author:
  • Post category:其他

在项目开发中,由于时间问题,往往会采用边开发边研讨的策略,在不断打磨中找到最合适的解决方案。目前接手的工作是一部分接口开发,为了统一请求报文格式、记录请求日志、记录请求报文等,还要考虑已开发接口的改造成本,于是想到了利用Spring AOP机制,通过注解的方式进行切入,实现接口的统一接入管理。

业务场景:

在核心系统中往往存在一系列的外部接口,供其他渠道、平台进行访问。此时对于接口报文的格式、接口调用的日志记录等容易被忽视。

解决方案:

  1. 通过创建自定义注解,在需要被管理的接口方法上进行标记
  2. 编写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、构造方法
}

注意事项:

  1. 使用AOP Around切入时,修改的参数类型必须和被切入的方法类型相同,否则在

    tResponseData = joinPoint.proceed(tParam);执行时会提示:无法将m类型转为n类型

    接收到的返回值 tResponseData 可以用Object暂时接纳,后续也只能强转为与被切入方法的返回值类型

    PS:本文案例统一使用Map为参数类型,String为返回值类型,大家可以根据需要自行调整

  2. 统一被切入方法入口参数后,对于后续操作不方便,可以使用JSON串中转为实际要处理的对象类型

    AikesBean tAikesBean = JSON.parseObject(JSON.toJSONString(tMap), AikesBean.class);

  3. 案例中涉及到部分业务操作代码有部分删减,敬请理解。


版权声明:本文为AikesLs原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。