通用分布式接口紧急开关

内容纲要

应用场景

  • 在微服务上线或者修改完成后更新后,可以对该服务中的一些接口进行接口放行或者接口关闭,做到紧急刹车的功能,以减少系统损失

实现方法

  1. 可以使用aop + 自定义注解 + Java反射 + redis完成
  2. 可以使用HandlerMethod + 自定义注解 + Java反射 + 拦截器 + redis完成

两种方案的对比

  1. aop 方案会在每个方式对象添加一层代理,编写简单
  2. HandlerMethod相对aop方案来说,HandlerMethod不需要使用到代理类,相对于代理来说性能会有所提升,但是编写难度大(本文实现方式)

实现思路

  • 在SpringMVC中RequestMappingHandlerMapping类负责处理@RequestMapping标记的Controller的方法,将每个方法转变为HandlerMethod并存储在mappingRegistry成员变量中,可以使用getHandlerMethods()方法获取
  • 获取到所有的HandlerMethod后则可以对每一个HandlerMethod进行判断是否存在@MappingSwitch注解如果存在则认为是需要处理的请求方法,将该方法的信息存储到MappingSwitchInfo类中,并存储到redis中,方便后续拦截器的数据获取
  • HandlerMethod处理完成后,则需要向SpringMVC中添加拦截器,以拦截HandlerMethod,判断HandlerMethod的方法或者类上是否存在@RequestMapping注解,如果存在则需要获取在redis中存储的信息判断当前接口是否运行通过

详细代码

  • MappingSwitchInfo.java

/**
 * MappingSwitch信息封装类
 * 
 * @author Clay
 * @date 2024/1/15  17:33
 */
@Data
public class MappingSwitchInfo {

    /**
     * 应用名称
     */
    private String applicationName;

    /**
     * 类名
     */
    private String className;

    /**
     * 方法名称
     */
    private String methodName;

    /**
     * 描述MappingSwitch注解的value可以为空
     */
    private String description;

    /**
     * HandlerMethod中的uri
     */
    private Set<String> uris;

    /**
     * 当前方法请求类型
     */
    private Set<String> httpMethods;

    /**
     * 当前方法的状态,true为正常放行,false为关闭
     */
    private Boolean state;

    public static class MappingSwitchConstant {
        /**
         * redis 的前缀
         */
        public static String MAPPING_SWITCH = "mapping:switch:";
    }

    /**
     * 获取redis key的方法
     * key 生成规则
     * 类注解:    {前缀}:{applicationName}:{className}
     * 方法注解:  {前缀}:{applicationName}:{className}:{methodStr}
     *
     * @param applicationName 应用名称
     * @param handlerMethod   请求方法
     * @param isMethod        是否为方法注解
     * @return key
     */
    public static String getKey(String applicationName, HandlerMethod handlerMethod, Boolean isMethod) {
        String packageName = handlerMethod.getBeanType().getPackage().getName() + ".";
        String name = handlerMethod.getBeanType().getName();
        String className = name.replace(packageName, "");
        String methodStr = "";
        if (isMethod) {
            Method method = handlerMethod.getMethod();
            StringJoiner joiner = new StringJoiner(", ", "(", ")");
            for (Class<?> paramType : method.getParameterTypes()) {
                joiner.add(paramType.getSimpleName());
            }
            methodStr = ":" + method.getName() + joiner;
        }
        return MappingSwitchInfo.MappingSwitchConstant.MAPPING_SWITCH + applicationName + ":" + className + methodStr;
    }
}
  • MappingSwitchService.java

/**
 * MappingSwitch开关处理类
 *
 * @author Clay
 * @date 2024/1/15  17:20
 */
@Slf4j
public class MappingSwitchService implements InitializingBean, ApplicationContextAware {

    private ApplicationContext applicationContext;

    private String applicationName = "";

    @Resource
    private RedisTemplate<String, MappingSwitchInfo> redisTemplate;

    @Override
    public void afterPropertiesSet() {
        RequestMappingHandlerMapping mapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
        Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
        Map<String, MappingSwitchInfo> mappingSwitchInfoMap = new HashMap<>();
        map.forEach((info, handlerMethod) -> {
            //判断方法上是否存在注解
            MappingSwitch method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), MappingSwitch.class);
            if (method != null) {
                MappingSwitchInfo mappingSwitchInfo = new MappingSwitchInfo();
                mappingSwitchInfo.setClassName(handlerMethod.getBeanType().getName());
                mappingSwitchInfo.setMethodName(handlerMethod.getMethod().getName());
                mappingSwitchInfo.setApplicationName(applicationName);
                mappingSwitchInfo.setState(Boolean.TRUE);
                mappingSwitchInfo.setDescription(method.value());
                //获取到uri
                PatternsRequestCondition patternsCondition = info.getPatternsCondition();
                if (patternsCondition != null) {
                    Set<String> uris = patternsCondition.getPatterns();
                    mappingSwitchInfo.setUris(uris);
                }
                //获取到请求类型
                RequestMethodsRequestCondition infoMethodsCondition = info.getMethodsCondition();
                Set<RequestMethod> methods = infoMethodsCondition.getMethods();
                if (!methods.isEmpty()) {
                    Set<String> methodSet = methods.stream().map(Enum::toString).collect(Collectors.toSet());
                    mappingSwitchInfo.setHttpMethods(methodSet);
                }
                //获取到当前的key
                String key = MappingSwitchInfo.getKey(applicationName, handlerMethod, Boolean.TRUE);
                //初始化
                initRedisCache(key, mappingSwitchInfo);
                //添加到临时缓存中
                mappingSwitchInfoMap.put(key, mappingSwitchInfo);
            }
            //判断类上是否存在注解
            MappingSwitch controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), MappingSwitch.class);
            if (controller != null) {
                //获取到key
                String key = MappingSwitchInfo.getKey(applicationName, handlerMethod, Boolean.FALSE);
                if (!mappingSwitchInfoMap.containsKey(key)) {
                    // 创建存储对象
                    MappingSwitchInfo mappingSwitchInfo = new MappingSwitchInfo();
                    mappingSwitchInfo.setClassName(handlerMethod.getBeanType().getName());
                    mappingSwitchInfo.setApplicationName(applicationName);
                    mappingSwitchInfo.setState(Boolean.TRUE);
                    mappingSwitchInfo.setDescription(controller.value());
                    //获取到RequestMapping,以获取到当前Controller的请求路径
                    RequestMapping requestMapping = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), RequestMapping.class);
                    if (requestMapping != null) {
                        String[] value = requestMapping.value();
                        Set<String> uris = new HashSet<>();
                        Collections.addAll(uris, value);
                        mappingSwitchInfo.setUris(uris);
                    }
                    //初始化对象
                    initRedisCache(key, mappingSwitchInfo);
                    //将对象添加到临时缓存
                    mappingSwitchInfoMap.put(key, mappingSwitchInfo);
                }
            }
        });
        if (!mappingSwitchInfoMap.isEmpty()) {
            redisTemplate.opsForValue().multiSet(mappingSwitchInfoMap);
        }
    }

    /**
     * 初始化缓存对象
     *
     * @param key  redis key
     * @param info 存储对象
     */
    private void initRedisCache(String key, MappingSwitchInfo info) {
        //获取redis中当前的key
        MappingSwitchInfo cacheInfo = redisTemplate.opsForValue().get(key);
        //如果redis中的状态为false,则不能更改redis中的状态
        if (cacheInfo != null && !cacheInfo.getState()) {
            info.setState(Boolean.FALSE);
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        Environment environment = applicationContext.getEnvironment();
        String applicationName = environment.getProperty("spring.application.name");
        if (ObjectUtils.isEmpty(applicationName)) {
            log.error("applicationName can not be null");
            throw new RuntimeException("applicationName can not be null");
        }
        this.applicationName = applicationName;
    }

}
  • MappingSwitchInterceptor.java

/**
 * MappingSwitch注解拦截器
 *
 * @author Clay
 * @date 2024/1/15  18:08
 */
@Slf4j
public class MappingSwitchInterceptor implements HandlerInterceptor {

    @Resource
    private Environment environment;

    @Resource
    private RedisTemplate<String, MappingSwitchInfo> redisTemplate;

    private String applicationName;

    @PostConstruct
    public void init() {
        String applicationName = environment.getProperty("spring.application.name");
        if (ObjectUtils.isEmpty(applicationName)) {
            log.error("applicationName can not be null");
            throw new RuntimeException("applicationName can not be null");
        }
        this.applicationName = applicationName;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前方法是否为HandlerMethod
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            //判断当前方法上是否有MappingSwitch注解
            MappingSwitch method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), MappingSwitch.class);
            if (method != null) {
                checkMethodState(handlerMethod, Boolean.TRUE);
            }
            //判断Controller类上是否有MappingSwitch注解
            MappingSwitch controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), MappingSwitch.class);
            if (controller != null) {
                checkMethodState(handlerMethod, Boolean.FALSE);
            }
        }
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    /**
     * 检查方法的装填
     *
     * @param handlerMethod 方法对象
     * @param isMethod      是否为方法, true为方法注解,false为Controller类注解
     */
    private void checkMethodState(HandlerMethod handlerMethod, Boolean isMethod) {
        MappingSwitchInfo mappingSwitchInfo = redisTemplate.opsForValue().get(MappingSwitchInfo.getKey(applicationName, handlerMethod, isMethod));
        if (mappingSwitchInfo != null && !mappingSwitchInfo.getState()) {
            throw new RuntimeException("当前接口关闭,请稍后再试");
        }
    }

}
THE END
分享
二维码
< <上一篇
下一篇>>