SpringBoot基于post转发热更新

背景

线上出现问题想修复,但是不想因为这么一个小问题就重启服务(因为长连接会断开又或者是影响用户使用),希望能不重启服务修复bug。

原理

项目中统一都是用了post请求接口,基本都会有@PostMapping注解。我们在PostMapping做一个切片,所有进入PostMapping的函数都先检查是否有热更代码,如果有就通过Groovy执行热更代码。

实现

复制代码
package com.meal.system.entity;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.meal.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import groovy.lang.Script;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;


@Data
@ApiModel(value = "热更代码")
@Accessors(chain = true)
@TableName(value = "sys_reload_code")
public class SysReloadCode extends BaseEntity {

    @ApiModelProperty("热更新路径")
    private String uri;

    @ApiModelProperty("执行函数名")
    private String funcName;

    @ApiModelProperty("执行函数")
    private String funcCode;

    //数据库不存在字段------------------------

    @ApiModelProperty(value = "代码",hidden = true)
    @TableField(exist = false)
    private Script script;

}


package com.meal.annotation.appbusiness;

import cn.hutool.core.collection.CollUtil;
import com.meal.common.vo.ResultVo;
import com.meal.constants.Constant;
import com.meal.system.entity.SysDict;
import com.meal.system.entity.SysReloadCode;
import com.meal.system.entity.SysUser;
import com.meal.utils.*;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;

import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

@Aspect
@Component
@Slf4j
public class HookAspect {

    public static Map<String, SysReloadCode> stringScriptMap = new ConcurrentHashMap<>();

    public static void reload() {
        List<SysReloadCode> sysReloadCodeList = EntityCrudServiceUtil.list(SysReloadCode.class, null);
        stringScriptMap.clear();
        for(SysReloadCode sysReloadCode : sysReloadCodeList) {
            //创建GroovyShell
            GroovyShell groovyShell = new GroovyShell();
            //装载解析脚本代码
            Script script = groovyShell.parse(sysReloadCode.getFuncCode());
            sysReloadCode.setScript(script);

            stringScriptMap.put(sysReloadCode.getUri(), sysReloadCode);
        }


    }

    private String getMatchKey(String uri) {
        if(ObjectUtil.isEmpty(stringScriptMap))
            return null;
        if (StringUtils.isBlank(uri)) {
            return null;
        }
        Set<String> pathList = stringScriptMap.keySet();

        for (String pattern : pathList) {
            if (UrlUtil.isMatch(pattern, uri)) {
                return pattern;
            }
        }
        return null;
    }

    @Around("@annotation(org.springframework.web.bind.annotation.PostMapping)")
    public Object methodExecution(ProceedingJoinPoint joinPoint) throws Throwable {

        HttpServletRequest request = null;
        try{
            request = ServletUtil.getRequest();
        }catch (Exception e){
            Object[] args = joinPoint.getArgs();
            return joinPoint.proceed(args);
        }

        String uri = request.getRequestURI();
        String key = this.getMatchKey(uri);

        SysReloadCode sysReloadCode = null;
        if(key != null){
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            PostMapping annotation = signature.getMethod().getAnnotation(PostMapping.class);
            String path = annotation.value()[0];
            if(key.contains(path)){
                sysReloadCode = stringScriptMap.get(key);
            }

        }

        Object[] args = joinPoint.getArgs();
        if(sysReloadCode == null){

            // 执行目标方法
            return joinPoint.proceed(args);
        } else {
            return sysReloadCode.getScript().invokeMethod(sysReloadCode.getFuncName(), args);
        }

    }


}

使用

把需要热更的代码和对应的路径添加到数据库,然后事先暴露reload接口,开发人员执行reload函数即可热更。如果热更代码有bug,只需要把对应的数据删了重新执行reload即可。

相关推荐
dpharness22 分钟前
踩完 dsh-ads 的四个坑,我说说虚构排名该怎么看
后端
张宏宇35 分钟前
我把微软开源的 tgrep 做成了本地版 GitHub Code Search:45,629 个文件里搜一次 10ms
前端·后端
用户0942485680338 分钟前
第20章:JVM G1 GC原理、日志与调优实战
java·jvm
打工仔折腾 AI42 分钟前
把模型切换交给平台:用蓝耘智能路由搭建商品评论分析工具
java·服务器·前端·后端·python·性能优化·ai agent 实战
后端LV44 分钟前
Caffeine 源码详解:为什么它是最快的 Java 本地缓存——一次压测引发的源码考古
java·后端
dd聊技术1 小时前
RAG 召回不准,先别急着换模型:用 RRF 把 BM25 和向量召回接起来
后端
dadaobusi1 小时前
Trace-driven 建模(基于轨迹/踪迹的建模)
java·后端·spring
136096757231 小时前
服务器回显里的四个假故障
后端
摇滚侠1 小时前
《Spring Boot 3:高级与架构设计》第 3 章 Bean 的全生命周期原理 Bean 的全生命周期概览 阅读笔记 9
spring boot·笔记·后端
墨家句子1 小时前
AnythingLLM 搭本地知识库:文档问答不准怎么调
linux·后端