个人周记丨2019-09-22在dao层使用aop对数据库查询的数据进行解密操作和统计查询中的四分位数

概述

在本周工作中,接到了两个非常有趣的需求。

  1. 在医疗保险项目中,mysql数据库中某些敏感字段将会被加密,当他人要查询这些数据时,我要将这些数据解密出来,并交个调用者。
  2. 在网关管理系统中,做一个关于服务响应时间的统计报表,统计报表的呈现形式为“箱线图”。

详细说明

  1. 数据解密
    医疗保险项目是一个中间件,它会提供一些接口,用来查询提供患者、医院等相关系统数据。它使用的数据库我们称之为主题库,即针对某项应用而所设计的数据集合,如本项目为医疗保险。在主题库的上层还有个库叫做标准库,标准库具有最全的数据。标准库的数据来自于原始库,而原始库中的数据来自于各大医院。在医疗保险项目的下层,还有一些中间件,他们做着类似于etl的工作,将标准库中的一些数据放到我所使用的主题库中。因需求变动,现在这些中间件会将一些敏感字段信息进行加密,然后放到我所使用的主题库中。而我在查询这些数据时,需要将这些数据解密,提供给调用我的接口的调用者。
  2. 箱线图
    网关管理项目是基于kong网关并根据公司的实际应用场景所开发的上层应用。用于kong网关相关参数配置以及报表分析。在报表分析中,需要求出每个“微服务”在某段时间内的最快响应时间,最慢响应时间,以及这段时间内的“上四分数”、“中位数”和“下四分数”。原型设计如下:
    在这里插入图片描述
    这里面的难点在于“四分数”的求取。

解题思路

1.数据解密
已知条件
  1. 首先,单纯的数据查询相关功能以及完成,但这不涉及到将解密。
  2. 我的下层提供了一个“加密配置”接口,这个接口会告诉我那些表的那些字段被加密了。
  3. 加密配置会存在变动,他们会将这些变动信息放到kafka中。
解决思路
  1. 项目启动后,我需要将所有的配置信息加载进来,即项目在启动后就调用他们提供的“加密配置”接口,然后将这些数据放在内存中。
  2. 监听kafka,一旦获取到配置变动信息,就对之前放置在内存中的“加密配置”信息进行更新。
  3. 在dao层设置aop,根据配置对数据库中获取的数据进行解密。
具体实现

步骤一:启动时就加载配置信息可以通过两种方式来完成。第一个是使用spring-boot 的事件模型,即实现ApplicationListener<ApplicationReadyEvent>并注入到spring容器中。第二个是实现CommandLineRunner 。本项目使用的第一个方式,实现逻辑如下:

1.自定义spring-boot监听器

@Component
public class ApplicationStartListener implements ApplicationListener<ApplicationReadyEvent> {

    Logger logger = LoggerFactory.getLogger(ApplicationStartListener.class);
	//远程调用服务
    @Autowired
    RpcService rpcService;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
        rpcService.loadEncryptionConfig();
    }
}

2.远程调用配置接口服务

@Service
public class RpcService {

    private Logger logger = LoggerFactory.getLogger(RpcService.class);

	//配置信息涉及到并发操作,故集合使用CopyOnWriteArrayList
    public static Map<String,CopyOnWriteArrayList<String>> encryptMap = new HashMap<>(100);

    @Autowired
    RestTemplate restTemplate;

    public void loadEncryptionConfig(){
        try {
            HttpEntity<String> httpEntity = new HttpEntity<>(null,new HttpHeaders());
            ResponseEntity<String> exchange = restTemplate.exchange(ENCRYPT_URL, HttpMethod.GET, httpEntity, String.class);
            //相应的业务逻辑
        } catch (RestClientException e) {
            logger.error("load encrypt config error");
        }

    }
}

3.kafka监听加密配置变动

@Component
public class MyKafkaListener {

    private static final Logger log = LoggerFactory.getLogger(MyKafkaListener.class);

    @KafkaListener(id = "${spring.kafka.consumer.client-id}", topics = "${spring.kafka.template.default-topic}")
    public void consumerListener(ConsumerRecord<String, String> record) {
        log.info("receive : " + record.toString());
      	//具体的业务逻辑 略
      	
    }
}

4.在dao层设置aop
这里有一个难点,就是程序如何知道从数据库中得到对象需要解密。这里我们可以使用自定义注解来告诉程序这个对于来源于某个表,如果这个表有加密配置,你应当对他的相关属性(对于与表的列)进行解密操作。

自定义注解 MyTableName


@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyTableName {

    String tableName() default "";

}

自定义注解 MyColumnName

//一些特殊的列可能需要改注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyColumnName {

    String columnName();
}

为pojo对象使用注解

public class MyPojo extends Model<MyPojo > {

    private static final long serialVersionUID = 1L;

    @TableId(value = "PKID", type = IdType.UUID)
    private String pkid;

	private String otherColumn;
}

为dao层使用aop

@Aspect
@Component
public class DaoAspect {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Pointcut("execution(public * com.cdqd.app.insurance.mapper.*.*.*(..))")
    public void daoPointcut() {
    }

    @Before("daoPointcut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {

    }

    @AfterReturning(returning = "ret", pointcut = "daoPointcut()")
    public void doAfterReturning(Object ret) throws Throwable {
        // 处理完请求,返回内容
        if (ret != null) {
            //返回的内容是list
            if (ret instanceof List) {
                List retList = (ArrayList) ret;
                for (Object pojo : retList) {
                    handPojo(pojo);
                }
            }
            //返回的内容是page
            else if (ret instanceof IPage){
                IPage retPage = (Page)ret;
                for (Object pojo : retPage.getRecords()) {
                    handPojo(pojo);
                }
            }
            //返回的内容是pojo
            else {
                handPojo(ret);
            }
        }

    }

    private void handPojo(Object ret) throws IllegalAccessException {
        Class<?> aClass = ret.getClass();

        //获取表注解
        MyTableName myTableName = aClass.getAnnotation(MyTableName.class);
        //查看表注解内容是否包含在配置中
        if (myTableName == null || encryptMap.get(myTableName.tableName()) == null) {
            return;
        }
        //包含则对属性进行处理
        Field[] fields = aClass.getDeclaredFields();
        for (Field field : fields) {
            List<String> columnConfig = encryptMap.get(myTableName.tableName());
            //获取列名
            String columnName = "";
            MyColumnName myColumnName = field.getAnnotation(MyColumnName.class);
            //从注解中获取
            if (myColumnName == null) {
                columnName = field.getName();
            } else {
                //使用属性名作为列名
                columnName = myColumnName.columnName();
            }
            //反射处理
            field.setAccessible(true);
            Object value = field.get(ret);
            //只对串类型进行处理,因为加密后产生的必定是串
            if (value instanceof String == false) {
                continue;
            }
            //解密
            for (String temp : columnConfig) {
                if (columnName.compareToIgnoreCase(temp.replaceAll("_", "")) == 0) {
                    try {
                        String newValue = EnDecryptionUtils.sm2Decrypt(value.toString());
                        field.set(ret, newValue);
                    }catch (Exception e){
                        logger.error("en decrypt fail for {}",value);
                    }
                }
            }
        }
    }

    /**
     * 获取请求方法
     *
     * @param jp
     * @return
     */
    public Method getInvokedMethod(JoinPoint jp) {

        // 被调用方法名称
        String methodName = jp.getSignature().getName();
        Method method = null;
        Method[] methods = jp.getTarget().getClass().getMethods();
        for (Method temp : methods) {
            if (temp.getName().equals(methodName)) {
                method = temp;
            }
        }
        return method;
    }

至此,这个需求就完成了。

2.查询四分位数
已知条件
  1. 个别微服务存在实时性接口,调用的次数一天甚至有数十万,一个微服务将产生近百万的访问记录,数据量随着时间会线性增长。
  2. 箱线图中每一个箱子统计结果由起止时间和截止时间节点。
解决思路

基于上面需求想到过3种方案。最开始想到的是将所有的响应时间全部查询出来,然后通过java代码进行计算,但想到如果用户选择的总时间段为30天(甚至更长),并分成30段(及箱线图显示成30个箱子),也就是说每个箱子的统计结果时间段位为1天,而一天的数据量近百万,纵使能够很快查询出来,但将查询的结果通过i/o返回到本地也会花费大量时间,用户并不能立即得到相应结果,故放弃了该方案。接着想到的就是定时任务完成,通过定时任务每30分钟计算出每个微服务的响应时间的箱线图数据,并创建一个张表将其保存下来,用户查询时从结果表中查询,但是这样存在一个很大的问题,他的最小时间粒度为30分钟,并且其他时间粒度必须为30分钟的倍数(1小时,1小时30分钟…),用户无法自定义显示箱线图箱子展示数,该值必须和前台约定写死。最后,想到一个好方法,通过多条sql分步查询出该结果,首先查出该段时间内的微服务的响应次数,然后通过嵌套查询和聚合函数limit就可以轻松得到相应的四分位数。

具体实现

1.箱线图实体

@ApiModel(value = "BoxPlot")
public class BoxPlot {

    /**
     * 组数
     */
    @ApiModelProperty("组数")
    private int boxNumber;

    /**
     * 组宽度 秒
     */
    @ApiModelProperty("组宽度 秒")
    private long boxWidth;

    @ApiModelProperty("箱线图数据")
    private List<Box> boxList;

    public BoxPlot() {
    }

    public BoxPlot(int boxNumber, long boxWidth, List<Box> boxList) {
        this.boxNumber = boxNumber;
        this.boxWidth = boxWidth;
        this.boxList = boxList;
    }

    public int getBoxNumber() {
        return boxNumber;
    }

    public void setBoxNumber(int boxNumber) {
        this.boxNumber = boxNumber;
    }

    public long getBoxWidth() {
        return boxWidth;
    }

    public void setBoxWidth(long boxWidth) {
        this.boxWidth = boxWidth;
    }

    public List<Box> getBoxList() {
        return boxList;
    }

    public void setBoxList(List<Box> boxList) {
        this.boxList = boxList;
    }
}

2.箱线图中的每个箱子实体

@ApiModel(value = "Box")
public class Box {
    /**
     * 上边缘
     */
    @ApiModelProperty("上边缘")
    private Integer top;

    /**
     * 下边缘
     */
    @ApiModelProperty("下边缘")
    private Integer bottom;

    /**
     * 中位数
     */
    @ApiModelProperty("中位数")
    private Integer median;

    /**
     * 上四分位
     */
    @ApiModelProperty("上四分位")
    private Integer topQuartile;

    /**
     * 下四分位
     */
    @ApiModelProperty("下四分位")
    private Integer bottomQuartile;

    /**
     * 横坐标 起始时间
     */
    @ApiModelProperty("起始时间")
    private LocalDateTime begin;

    /**
     * 横坐标 终止时间
     */
    @ApiModelProperty("终止时间")
    private LocalDateTime end;

    public Box() {
    }

    public Box(Integer top, Integer bottom, Integer median, LocalDateTime begin, LocalDateTime end) {
        this.top = top;
        this.bottom = bottom;
        this.median = median;
        this.begin = begin;
        this.end = end;
    }

    public Integer getTop() {
        return top;
    }

    public void setTop(Integer top) {
        this.top = top;
    }

    public Integer getBottom() {
        return bottom;
    }

    public void setBottom(Integer bottom) {
        this.bottom = bottom;
    }

    public Integer getMedian() {
        return median;
    }

    public void setMedian(Integer median) {
        this.median = median;
    }

    public Integer getTopQuartile() {
        return topQuartile;
    }

    public void setTopQuartile(Integer topQuartile) {
        this.topQuartile = topQuartile;
    }

    public Integer getBottomQuartile() {
        return bottomQuartile;
    }

    public void setBottomQuartile(Integer bottomQuartile) {
        this.bottomQuartile = bottomQuartile;
    }

    public LocalDateTime getBegin() {
        return begin;
    }

    public void setBegin(LocalDateTime begin) {
        this.begin = begin;
    }

    public LocalDateTime getEnd() {
        return end;
    }

    public void setEnd(LocalDateTime end) {
        this.end = end;
    }
}

3.controller

@Controller
@RequestMapping("accessLog")
@Api(value = "统计", tags = "AccessLogController",description = "应用服务统计分析接口")
public class AccessLogController {

    @Autowired
    IAccessLogService accessLogService;


    @ApiOperation("服务响应图")
    @RequestMapping(value = "service/responseAnalysis", method = RequestMethod.GET)
    @ResponseBody
    public MyResponseEntity<BoxPlot> responseAnalysis(
            @ApiParam("服务id") @RequestParam("id") String id,
            @ApiParam(value = "开始时间") @RequestParam("begin") LocalDateTime begin,
            @ApiParam(value = "截止时间") @RequestParam("end") LocalDateTime end,
            @ApiParam(value = "分段数 5 <= number <= 30") @RequestParam("number") int number) {
        if (StringUtils.isEmpty(id)) {
            return new MyResponseEntity<>(ERROR_CODE, "服务id丢失");
        }
        if (number < MIN_SECTION || number > MAX_SECTION) {
            return new MyResponseEntity<>(ERROR_CODE, "分段数不合理");
        }
        if (end.isBefore(begin)) {
            return new MyResponseEntity<>(ERROR_CODE, "开始时间大于截止时间");
        }

        BoxPlot boxPlot = accessLogService.responseAnalysis(id, begin, end, number);
        return new MyResponseEntity().buildSuccess("boxPlot", boxPlot);
    }
}

4.service

@Service
public class AccessLogServiceImpl extends ServiceImpl<AccessLogMapper, AccessLog> implements IAccessLogService {

    @Autowired
    AccessLogMapper accessLogMapper;
 @Override
    public BoxPlot responseAnalysis(String id, LocalDateTime begin, LocalDateTime end, int number) {
        //时间总长度 second
        long length = (datetimeToEpochMilli(end) - datetimeToEpochMilli(begin)) / 1000;
        //每段时间长度 second
        long sectionLength = length / number;

        //每段时间的起点和终点
        LocalDateTime beginTemp = begin;
        LocalDateTime endTemp = null;

        //箱型图对象
        BoxPlot boxPlot = new BoxPlot(number, sectionLength, null);
        List<Box> boxList = new ArrayList<>(20);
        while (number > 0) {
            number--;
            if (number == 0) {
                endTemp = end;
            } else {
                endTemp = beginTemp.plusSeconds(sectionLength);
            }
            //sql query
            //查询总访问量,用于辅助四分位数的查询
            Integer countForService = accessLogMapper.accessCountForService(id, beginTemp, endTemp);
            //查询下边缘
            Integer bottomResponse = accessLogMapper.minResponse(id, beginTemp, endTemp);
            //查询上边缘
            Integer topResponse = accessLogMapper.maxResponse(id, beginTemp, endTemp);
            //查询上四分位
            Integer topQuartileResponse = accessLogMapper.quartilResponse(id, beginTemp, endTemp, countForService / 4);
           	//查询中位数
            Integer medianResponse = accessLogMapper.quartilResponse(id, beginTemp, endTemp, countForService / 2);
            //查询下四分位
            Integer bottomQuartileResponse = accessLogMapper.quartilResponse(id, beginTemp, endTemp, countForService - (countForService / 4));
            Box box = new Box();
            box.setBegin(beginTemp);
            box.setEnd(endTemp);
            box.setTop(topResponse);
            box.setBottom(bottomResponse);
            box.setTopQuartile(topQuartileResponse);
            box.setMedian(medianResponse);
            box.setBottomQuartile(bottomQuartileResponse);
            boxList.add(box);
            beginTemp = beginTemp.plusSeconds(sectionLength);
        }
        //组装
        boxPlot.setBoxList(boxList);
        return boxPlot;
    }
}

4.四分位SQL(mybatis)

    <select id="quartilResponse" resultType="java.lang.Integer">
        select request_time
        from (
        select request_time
        from access_log
        where access_time &gt; #{begin} and access_time &lt; #{end} and service_id = #{id}
        order by request_time asc
        ) as result
        limit #{offset},1
    </select>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值