设计模式-责任链模式

概念

责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。

场景

现实中,请假的OA申请,请假天数如果是半天到1天,可能直接主管批准即可;
如果是1到3天的假期,需要部门经理批准;
如果是3天到30天,则需要总经理审批;
大于30天,正常不会批准。

类图

为了实现上述场景,我们可以采用责任链设计模式。

1、员工提交请求类:LeaveRequest。
2、抽象的请假责任处理类:AbstractLeaveHandler。
3、直接主管审批处理类:DirectLeaderLeaveHandler。
4、部门经理处理类:DeptManagerLeaveHandler。
5、总经理处理类: GManagerLeaveHandler。

员工请求发起申请到抽象的责任处理类中,根据员工的请假天数,对应的处理类完成处理。
每一个责任处理类设置下面的节点。自身处理不了则传递给下一个节点处理。
在这里插入图片描述

代码实现

LeaveRequest.java:

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LeaveRequest {
    /**天数*/
    private int leaveDays;

    /**姓名*/
    private String name;
}

AbstractLeaveHandler.java

/**
 * @program: cookbook
 * @description: 请假责任链抽象处理类
 * @author: Byron
 * @create: 2019/07/11 09:34
 */
public class AbstractLeaveHandler {
    /**直接主管审批处理的请假天数*/
    protected int MIN = 1;
    /**部门经理处理的请假天数*/
    protected int MIDDLE = 3;
    /**总经理处理的请假天数*/
    protected int MAX = 30;

    /**领导名称*/
    protected String handlerName;

    /**下一个处理节点(即更高级别的领导)*/
    protected AbstractLeaveHandler nextHandler;

    /**设置下一节点*/
    protected void setNextHandler(AbstractLeaveHandler handler){
        this.nextHandler = handler;
    }

    /**处理请假的请求,子类实现*/
    protected void handlerRequest(LeaveRequest request){

    }


}

DirectLeaderLeaveHandler.java

/**
 * @program: cookbook
 * @description: 直接主管处理类
 * @author: Byron
 * @create: 2019/07/11 09:46
 */
public class DirectLeaderLeaveHandler extends AbstractLeaveHandler{
    public DirectLeaderLeaveHandler(String name) {
        this.handlerName = name;
    }

    @Override
    protected void handlerRequest(LeaveRequest request) {
        if(request.getLeaveDays() <= this.MIN){
            System.out.println("直接主管:" + handlerName + ",已经处理;流程结束。");
            return;
        }

        if(null != this.nextHandler){
            this.nextHandler.handlerRequest(request);
        }else{
            System.out.println("审批拒绝!");
        }

    }
}

DeptManagerLeaveHandler.java

/**
 * @program: cookbook
 * @description: 部门经理处理类
 * @author: Byron
 * @create: 2019/07/11 09:48
 */
public class DeptManagerLeaveHandler extends AbstractLeaveHandler {

    public DeptManagerLeaveHandler(String name) {
        this.handlerName = name;
    }

    @Override
    protected void handlerRequest(LeaveRequest request) {
        if(request.getLeaveDays() >this.MIN && request.getLeaveDays() <= this.MIDDLE){
            System.out.println("部门经理:" + handlerName + ",已经处理;流程结束。");
            return;
        }

        if(null != this.nextHandler){
            this.nextHandler.handlerRequest(request);
        }else{
            System.out.println("审批拒绝!");
        }
    }
}

GManagerLeaveHandler.java

/**
 * @program: cookbook
 * @description: 总经理处理类
 * @author: Byron
 * @create: 2019/07/11 09:49
 */
public class GManagerLeaveHandler extends AbstractLeaveHandler {
    public GManagerLeaveHandler(String name) {
        this.handlerName = name;
    }

    @Override
    protected void handlerRequest(LeaveRequest request) {
        if(request.getLeaveDays() > this.MIDDLE && request.getLeaveDays() <= this.MAX){
            System.out.println("总经理:" + handlerName + ",已经处理;流程结束。");
            return;
        }

        if(null != this.nextHandler){
            this.nextHandler.handlerRequest(request);
        }else{
            System.out.println("审批拒绝!");
        }
    }
}

演示

ResponsibilityTest.java

public class ResponsibilityTest {
    public static void main(String[] args) {
        LeaveRequest request = LeaveRequest.builder().leaveDays(20).name("小明").build();


        AbstractLeaveHandler directLeaderLeaveHandler = new DirectLeaderLeaveHandler("县令");
        DeptManagerLeaveHandler deptManagerLeaveHandler = new DeptManagerLeaveHandler("知府");
        GManagerLeaveHandler gManagerLeaveHandler = new GManagerLeaveHandler("京兆尹");

        directLeaderLeaveHandler.setNextHandler(deptManagerLeaveHandler);
        deptManagerLeaveHandler.setNextHandler(gManagerLeaveHandler);

        directLeaderLeaveHandler.handlerRequest(request);


    }
}
  • 20天,运行输出: 总经理:京兆尹,已经处理;流程结束。
  • 1天,运行输出: 直接主管:县令,已经处理;流程结束。
  • 3天,运行输出: 部门经理:知府,已经处理;流程结束。
  • 35天,运行输出: 审批拒绝!

上面的代码写法上可以优化下比如:

/**
*  用「责任链模式」来优化 参数名重校验,非常优雅!
*  之前在做需求,写一个方法,先在前面做验证,
*  if不满足A条件则return,
*  if不满足B条件则return...
*  一共写了5个验证,等验证通过以后才执行下面1*过了一阵产品提了需求,跟这个方法类似,
*  我又把这个方法copy了一份,只不过验证条件稍微有点不一样,变成6个验证了。
/
Handler.Builder builder = new Handler.Builder()
//链式编程,谁在前谁在后看的清清楚楚
builder .addHandler(new ValidateHandler())
		.addHandler(new LoginHandler())
		.addHandler(new AuthHandler())
		.addHandler(new BusinessLogicHandler())
User user = new User()
user .setUserName("woniu" )
user.setPassWord("666")
builder.build().doHandler(user);

public class AuthHandler extends Handler{

	public void doHandler(User user){
		if(!"管理员".equals(user.getRoleName())){System.out.println("您不是管理员,没有操作权限")return;
		if(null != next)
		next.doHandler(user);
   }
}

总结

责任链主要重在责任分离处理,让各个节点各司其职。
责任链上的各个节点都有机会处理事务,但是也可能不会受理请求。
责任链比较长,调试时可能会比较麻烦。
责任链一般用于处理流程节点之类的实际业务场景中。
Spring拦截器链、servlet过滤器链等都采用了责任链设计模式。

PS:上面是标准的责任链模式,具体实现根据业务场景进行变化,并不是千篇一律的,比如就上面的业务而言,还可以做到严谨些,比如将每个实现类中handlerRequest(处理请求业务)提取到抽象类中,如果每个实现类不一致的话,可以下沉到各个子类中,当然这里显然不是,比如下面的代码块

    /**
     * 责任链模式-抽象类
     */
    @Data
    static private abstract class BaseProcessingObject<T> {

        private BaseProcessingObject<T> successor;

        public T handle(T input) {
            T t = handleWork(input);
            if (successor != null) {
                return successor.handle(t);
            }
            return t;
        }

        abstract protected T handleWork(T input);
    }
    /**
     * ecu_install_time : 查询 vehicle_log_ecu_report -> 存入 vehicle_log_task_ecu_result、strategy_part
     */
    private class ProcessingEcuTime extends BaseProcessingObject<VehicleLogEcuReportEntity> {
        @Override
        protected VehicleLogEcuReportEntity handleWork(VehicleLogEcuReportEntity input) {

            // 策略测试详情下,每一 vin 的每一 ecu 预计升级安装耗时
            List<VehicleLogEcuReportEntity> dbVinEcus = vehicleLogEcuReportMapper.selectVinEcuList(input);
            if (dbVinEcus == null || dbVinEcus.isEmpty()) {
                return null;
            }
            Map<String, List<VehicleLogEcuReportEntity>> strategyVinEcuPartMap =
                    dbVinEcus.stream().collect(Collectors.groupingBy((VehicleLogEcuReportEntity vehicleLogEcuReportEntity) -> vehicleLogEcuReportEntity.getPartId().toString()));
            for (Map.Entry<String, List<VehicleLogEcuReportEntity>> entryPart : strategyVinEcuPartMap.entrySet()) {
                List<VehicleLogEcuReportEntity> ecu = entryPart.getValue().parallelStream().filter(e -> e.getEcuTime() != null && e.getEcuTime() != 0).collect(Collectors.toList());
                if (ecu.isEmpty()) {
                    continue;
                }
                input.setPartNumber(ecu.get(0).getPartNumber());
                input.setEcuDid(ecu.get(0).getEcuDid());
                input.setEcuSwid(ecu.get(0).getEcuSwid());
                int avgPartVin = (int) Math.ceil(ecu.parallelStream().collect(Collectors.averagingInt(VehicleLogEcuReportEntity::getEcuTime)) * EcuConstant.ECU_TIME_MULTIPLE);
                input.setStrategyEcuVinTime(avgPartVin);
                logger.info("策略Id:[{}],vin:[{}],零件Id:[{}],预计升级时间:[{}]min", input.getStrategyId(), input.getVin(), ecu.get(0).getPartId(), avgPartVin);
                vehicleLogEcuReportMapper.updateVehicleLogTaskEcuTime(input);
            }

            // 策略下所有测试车辆的每一 ecu 预计升级安装耗时
            List<VehicleLogEcuReportEntity> dbEcus = vehicleLogEcuReportMapper.selectProcessEcuList(input);
            if (dbEcus == null || dbEcus.isEmpty()) {
                return null;
            }
            // 分组:以 partId 为桶收集元素
            Map<String, List<VehicleLogEcuReportEntity>> strategyEcuPartMap =
                    dbEcus.stream().collect(Collectors.groupingBy((VehicleLogEcuReportEntity vehicleLogEcuReportEntity) -> vehicleLogEcuReportEntity.getPartId().toString()));
            int total = 0;
            // 此时为同一 session, 即同一策略 id 下, 以零件 id 为 key, 遍历 vehicle_log_ecu_report 中取出每一次 ecu 安装成功时间的集合
            for (Map.Entry<String, List<VehicleLogEcuReportEntity>> entrys : strategyEcuPartMap.entrySet()) {
                // 在不要求集合有序的情况下,并行 parallelStream() 比串行 stream() 在多核条件下效率更高
                List<VehicleLogEcuReportEntity> ecus = entrys.getValue().parallelStream().filter(e -> e.getEcuTime() != null && e.getEcuTime() != 0).collect(Collectors.toList());
                if (ecus.isEmpty()) {
                    continue;
                }
                int avgPart = (int) Math.ceil(ecus.parallelStream().collect(Collectors.averagingInt(VehicleLogEcuReportEntity::getEcuTime)) * EcuConstant.ECU_TIME_MULTIPLE);
                total += avgPart;
                StrategyPartEntity strategyPartEntity = new StrategyPartEntity();
                strategyPartEntity.setPartId(ecus.get(0).getPartId());
                strategyPartEntity.setStrategyId(input.getStrategyId());
                strategyPartEntity.setStrategyEcuTime(avgPart);
                logger.info("策略Id:[{}],零件Id:[{}]预计升级时间:[{}]min", input.getStrategyId(), ecus.get(0).getPartId(), avgPart);
                vehicleLogEcuReportMapper.updateStrategyPartEcuTime(strategyPartEntity);
            }
            // 策略预计升级的总时间
            input.setStrategyTime(total);
            return input;
        }
    }
    /**
     * strategy_time : 后继, 承接上一方法的工作, 传入总时间, 更新 strategy_info 表
     */
    private class ProcessingStrategyTime extends BaseProcessingObject<VehicleLogEcuReportEntity> {
        @Override
        protected VehicleLogEcuReportEntity handleWork(VehicleLogEcuReportEntity input) {
            logger.info("策略id:[{}]预计升级时间:[{}]min", input.getStrategyId(), input.getStrategyTime());
            vehicleLogEcuReportMapper.updateStrategyUpgradeTime(input);

            // kafka 发送消息,同步缓存
            HashMap<String, String> params = new HashMap<>(1);
            params.put("id", input.getStrategyId().toString());
            ModifyRedisModel modifyRedisModel = new ModifyRedisModel();
            modifyRedisModel.setMethodId(Constant.METHOD_NUM_BIND_STRATEGY);
            modifyRedisModel.setTrigger(Constant.TRIGGER_SEND_TO_KAFAK_INTERFACE);
            modifyRedisModel.setParams(params);
            kafkaProducer.sendModifyRedisLog(JSON.toJSONString(modifyRedisModel));
            return null;
        }
    }

调用者

            ProcessingEcuTime processingEcuTime = new ProcessingEcuTime();
            ProcessingStrategyTime processingStrategyTime = new ProcessingStrategyTime();
            processingEcuTime.setSuccessor(processingStrategyTime);
            VehicleLogEcuReportEntity param = new VehicleLogEcuReportEntity();
            param.setStrategyId(session.getStrategyId());
            param.setVin(session.getVin());
            processingEcuTime.handle(param);

我们项目中都有很多接口访问日志打印、验签、加解密、业务处理等一些前置动作,而这些可以形成责任链模式,比如现在有个业务场景:请求参数日志打印 -> 非注册接口的车辆信息获取 -> 验签 -> ecuDids 解析 -> ecuPartNum 解析 -> exts 解析

package com.abupdate.core.aop;

import com.common.constant.ApiEnum;
import com.common.constant.ResultEnum;
import com.common.dto.BaseDTO;
import com.common.exception.BusinessException;
import com.common.vo.BaseVO;
import com.core.service.impl.GenerateReturnSign;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Maps;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Map;

/**
 * <p>AOP 类,包含请求/响应日志打印、签名验证、车辆信息校验等</p>
 * <p>需要留意下 AOP 的执行顺序:</p>
 * <ul>
 *     <li>正常情况下:@Around -&gt; @Before -&gt; Method -&gt; @Around -&gt; @After -&gt; @AfterReturning</li>
 *     <li>异常情况下:@Around -&gt; @Before -&gt; Method -&gt; @Around -&gt; @After -&gt; @AfterThrowing</li>
 * </ul>
 *
 * @author fh
 * @date 2021-06-21 14:27
 */
@Aspect
@Component
@Order(0)
@Slf4j
@RequiredArgsConstructor
public class RequestAop {

    /**
     * 请求参数日志打印
     */
    private final RequestLogPrint p1;

    /**
     * 非注册接口的车辆信息获取
     */
    private final GetVehicleInfo p2;

    /**
     * 请求参数签名验证
     */
    private final ValidateSign p3;

    /**
     * ecuDids 参数解析
     */
    private final EcuDidsParse p4;

    /**
     * 获取零件编号
     */
    private final GetPartNum p5;

    /**
     * exts 参数解析
     */
    private final ExtsParse p6;

    /**
     * 返回报文签名生成
     */
    private final GenerateReturnSign generateReturnSign;

    /**
     * Key:apiName 接口方法名 Value:apiDesc 接口方法名中文描述
     */
    private final static Map<String, String> METHOD_MAP = Maps.newHashMapWithExpectedSize(ApiEnum.values().length);

    static {
        METHOD_MAP.put(ApiEnum.REGISTER.getMethodName(), ApiEnum.REGISTER.getApiName());
        METHOD_MAP.put(ApiEnum.CURRENT_TIME.getMethodName(), ApiEnum.CURRENT_TIME.getApiName());
        METHOD_MAP.put(ApiEnum.GET_CLIENT_CONFIG.getMethodName(), ApiEnum.GET_CLIENT_CONFIG.getApiName());
        METHOD_MAP.put(ApiEnum.REPORT_VEHICLE_CONFIG.getMethodName(), ApiEnum.REPORT_VEHICLE_CONFIG.getApiName());
        METHOD_MAP.put(ApiEnum.CHECK.getMethodName(), ApiEnum.CHECK.getApiName());
        METHOD_MAP.put(ApiEnum.STATE_REPORT.getMethodName(), ApiEnum.STATE_REPORT.getApiName());
        METHOD_MAP.put(ApiEnum.ECU_STATE_REPORT.getMethodName(), ApiEnum.ECU_STATE_REPORT.getApiName());
        METHOD_MAP.put(ApiEnum.PROGRESS_REPORT.getMethodName(), ApiEnum.PROGRESS_REPORT.getApiName());
        METHOD_MAP.put(ApiEnum.FILE_UPLOAD.getMethodName(), ApiEnum.FILE_UPLOAD.getApiName());
        METHOD_MAP.put(ApiEnum.EVENT_REPORT.getMethodName(), ApiEnum.EVENT_REPORT.getApiName());
        METHOD_MAP.put(ApiEnum.GET_UPGRADE_HISTORY.getMethodName(), ApiEnum.GET_UPGRADE_HISTORY.getApiName());
    }

    /**
     * 对 Controller 做切点
     */
    @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
    public void pointcut() {
    }

    @Before("pointcut()")
    public void doBefore(JoinPoint joinPoint) {

        // 接口形参列表
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg instanceof BaseDTO) {
                BaseDTO baseDTO = (BaseDTO) arg;
                String methodName = currentMethod(joinPoint);

                // 请求接口名称
                String apiName = METHOD_MAP.get(methodName);

                // 请求参数日志打印 -> 非注册接口的车辆信息获取 -> 验签 -> ecuDids 解析 -> ecuPartNum 解析 -> exts 解析
                // 设置调用顺序
                p1.setSuccessor(p2);
                p2.setSuccessor(p3);
                p3.setSuccessor(p4);
                p4.setSuccessor(p5);
                p5.setSuccessor(p6);

                // 开始按顺序链式处理
                p1.handle(baseDTO, apiName, args);

                break;
            }
        }
    }

    @AfterReturning(pointcut = "pointcut()", returning = "returnValue")
    public void doAfterReturn(JoinPoint joinPoint, Object returnValue) {

        String methodName = currentMethod(joinPoint);
        String apiName = METHOD_MAP.get(methodName);
        if (apiName == null) {
            log.error("请求 Url 不存在");
            throw new BusinessException(ResultEnum.PARAM_VERIFICATION_FAIL, "请求 Url 不存在");
        }

        if (returnValue == null) {
            log.error("{} 接口返回值为 null,请检查代码", apiName);
            throw new BusinessException(ResultEnum.UNKNOWN_ERROR);
        }

        BaseVO baseVO = (BaseVO) returnValue;
        String sign = generateReturnSign.initReturnSign(baseVO, null);
        baseVO.setSign(sign);

        // 正常响应参数日志打印
        log.info(apiName + "响应:" + JSON.toJSONString(baseVO));
    }

    private String currentMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Method[] methods = joinPoint.getTarget().getClass().getMethods();
        Method resultMethod = null;
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                resultMethod = method;
                break;
            }
        }

        return resultMethod == null ? "" : resultMethod.getName();
    }

}

1、RequestLogPrint

package com.core.aop;

import com.common.dto.BaseDTO;
import com.core.service.BaseProcessingObject;
import com.alibaba.fastjson.JSON;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import org.springframework.stereotype.Component;

/**
 * 请求参数打印
 *
 * @author fh
 * @date 2021-05-17 11:02
 */
@Component
@Slf4j
@Setter
public class RequestLogPrint extends BaseProcessingObject<BaseDTO> {

    @Override
    protected BaseDTO handleWork(BaseDTO input, String apiName, Object[] args) {

        log(JSON.toJSONString(input), apiName);
        return input;
    }

    private void log(String json, String apiName) {
        log.info(apiName + "请求:" + json);
    }
}

2、GetVehicleInfo

package com.core.aop;

import com.common.config.MqConfig;
import com.common.constant.ApiEnum;
import com.common.constant.MqConstant;
import com.common.constant.ResultEnum;
import com.common.constant.VehicleConstant;
import com.common.dto.BaseDTO;
import com.common.dto.CoreMsgDTO;
import com.common.entity.VehicleInfoDO;
import com.common.exception.BusinessException;
import com.core.config.OtaConfig;
import com.core.mq.Mq;
import com.core.service.GetVehicleInfoService;
import com.core.service.BaseProcessingObject;
import com.alibaba.fastjson.JSON;

import lombok.RequiredArgsConstructor;
import lombok.Setter;

import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 非注册接口的车辆信息获取
 *
 * @author fh
 * @date 2021-04-27 17:29
 */
@Component
@RequiredArgsConstructor
@Setter
public class GetVehicleInfo extends BaseProcessingObject<BaseDTO> {

    private final GetVehicleInfoService getVehicleInfoService;

    private final OtaConfig otaConfig;

    private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

    private final Mq mq;

    private final MqConfig mqConfig;

    @Override
    protected BaseDTO handleWork(BaseDTO input, String apiName, Object[] args) {

        // 注册接口
        if (ApiEnum.REGISTER.getApiName().equals(apiName)) {
            return input;
        }

        // 非注册接口
        String deviceId = (String) args[1];
        VehicleInfoDO vehicle = getVehicleInfoService.getVehicleInfoByDeviceId(deviceId).getData();
        updateVehicleLivelyTime(vehicle);
        validOtaFun(vehicle.getOtaFunStatus());
        fillRequestParam(input, vehicle);

        return input;
    }

    private void validOtaFun(Integer status) {

        // 关闭 "OTA 功能是否开启"校验
        if (!otaConfig.getEnable()) {
            return;
        }

        if (status == null || VehicleConstant.OTA_CLOSED.equals(status)) {
            throw new BusinessException(ResultEnum.OTA_FUN_CLOSED);
        }
    }

    private void updateVehicleLivelyTime(VehicleInfoDO vehicleInfo) {
        Date livelyTime = vehicleInfo.getLivelyTime();
        Date now = new Date();

        // 车辆当天第一次访问车云服务
        if (livelyTime == null || !dateFormat.format(now).equals(dateFormat.format(livelyTime))) {
            // 更新活跃时间为当前时间
            vehicleInfo.setLivelyTime(now);
            mq.sendData(mqConfig.getCoreInterfaceTopic(), vehicleInfo.getVin(), CoreMsgDTO.builder()
                .type(MqConstant.LIVELY)
                .json(JSON.toJSONString(vehicleInfo))
                .build());
        }
    }

    /**
     * 填充请求参数
     *
     * @param input   请求参数
     * @param vehicle 车辆信息
     */
    private void fillRequestParam(BaseDTO input, VehicleInfoDO vehicle) {

        // 客户端上报时间
        vehicle.setClientTime(new Date(input.getTimestamp() * 1000));
        // 服务器开始处理请求的开始时间
        vehicle.setServerTime(new Date());

        // 填充 deviceId
        input.setDeviceId(vehicle.getDeviceId());
        // 填充车辆信息
        input.setVehicle(vehicle);
    }
}

3、ValidateSign

package com.core.aop;

import com.common.constant.ApiEnum;
import com.common.constant.MdcConstant;
import com.common.constant.ResultEnum;
import com.common.dto.BaseDTO;
import com.common.dto.RegisterDTO;
import com.common.exception.BusinessException;
import com.core.config.SignConfig;
import com.core.service.BaseProcessingObject;
import com.core.util.SignUtil;
import com.alibaba.fastjson.JSON;

import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import cn.hutool.crypto.SecureUtil;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 请求参数签名验证
 *
 * @author fh
 * @date 2021-04-27 17:28
 */
@Component
@Setter
@RequiredArgsConstructor
@Slf4j
public class ValidateSign extends BaseProcessingObject<BaseDTO> {

    /**
     * 签名相关配置
     */
    private final SignConfig sign;

    private static final Pattern VEHICLE_ECUS_PATTERN = Pattern.compile("ecus\\[\\d+].*ecus\\[\\d+](.*?(?=&))");

    private static final Pattern ECU_STATE_ECUS_PATTERN = Pattern.compile("reportEcus\\[\\d+].*reportEcus\\[\\d+](.*?" +
        "(?=&))");

    @Override
    protected BaseDTO handleWork(BaseDTO input, String apiName, Object[] args) {

        // 报文签名功能已关闭,直接返回
        if (!sign.getEnable()) {
            return input;
        }

        String key;

        // 注册接口
        if (ApiEnum.REGISTER.getApiName().equals(apiName)) {
            key = ((RegisterDTO) input).getVin();
        } else {
            // 非注册接口
            key = input.getVehicle().getSecret();
        }

        saveReturnSignKey(key);

        // 签名内容
        String signContent = SignUtil.generateSignContent(JSON.toJSONString(input));

        validSign(signContent, key, input.getSign());

        // 如果是检测接口,需要计算 bodyHash
        if (ApiEnum.CHECK.getApiName().equals(apiName)) {
            String bodyHash = SecureUtil.md5(signContent.replaceAll("timestamp=\\d*", ""));
            input.setBodyHash(bodyHash);
        }

        // 如果是上报客户端配置接口、检测接口、Ecu 状态接口(包含 Ecu Dids 上报的接口),需要计算 Ecus Hash
        if (ApiEnum.REPORT_VEHICLE_CONFIG.getApiName().equals(apiName)
            || ApiEnum.CHECK.getApiName().equals(apiName)) {
            Matcher matcher = VEHICLE_ECUS_PATTERN.matcher(signContent);
            if (matcher.find()) {
                input.setEcusHash(SecureUtil.md5(matcher.group()));
            }
            return input;
        }
        if (ApiEnum.ECU_STATE_REPORT.getApiName().equals(apiName)) {
            Matcher matcher = ECU_STATE_ECUS_PATTERN.matcher(signContent);
            if (matcher.find()) {
                input.setEcusHash(SecureUtil.md5(matcher.group()));
            }
        }

        return input;
    }

    /**
     * 校验客户端上报签名是否正确
     *
     * @param signContent 签名拼接内容
     * @param key         签名秘钥
     * @param clientSign  客户端上报的签名
     * @throws BusinessException 签名验证失败
     */
    private void validSign(String signContent, String key, String clientSign) {

        if (log.isDebugEnabled()) {
            log.debug("签名内容:{}", signContent);
        }

        String serverSign = SignUtil.generateSign(signContent, key);

        if (!serverSign.equals(clientSign)) {
            // 签名不正确
            log.warn("服务端生成签名:{},客户端生成签名:{},服务端签名 Key:{}", serverSign, clientSign, key);
            throw new BusinessException(ResultEnum.SIGN_AUTHENTICATION_FAIL);
        }
    }

    /**
     * 保存返回报文签名 Key 到线程变量中
     *
     * @param key 签名 Key
     */
    private void saveReturnSignKey(String key) {
        MDC.put(MdcConstant.SIGN_KEY, key);
    }
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值