SpringBoot + 自定义注解 + AOP 打造通用开关

news2024/11/18 20:45:51

前言

最近在工作中迁移代码的时候发现了以前自己写的一个通用开关实现,发现挺不错,特地拿出来分享给大家。

为了有良好的演示效果,我特地重新建了一个项目,把核心代码提炼出来加上了更多注释说明,希望xdm喜欢。

案例

1、项目结构

2、引入依赖
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
        </dependency>
    </dependencies>
3、yml配置

连接Redis的配置修改成自己的

server:
  port: 8888

spring:
  redis:
    database: 6
    host: XX.XX.XX.XX
    port: 6379
    password:
    jedis:
      pool:
        max-active: 100
        max-wait: -1ms
        max-idle: 50
        min-idle: 1
4、自定义注解

这里稍微说明下,定义了一个key对应不同功效的开关,定义了一个val作为开关是否打开的标识,以及一个message作为消息提示。

package com.wang.annotation;
import com.wang.constant.Constant;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * @projectName: spring-study
 * @package: com.wang.annotation
 * @className: ServiceSwitch
 * @author: wangwujie
 * @description: TODO
 * @date: 2024-1-18 10:30
 */

@Target({ElementType.METHOD})  // 作用在方法上
@Retention(RetentionPolicy.RUNTIME)  // 运行时起作用
public @interface ServiceSwitch {

    /**
     * 业务开关的key(不同key代表不同功效的开关)
     * {@link Constant.ConfigCode}
     */
    String switchKey();

    // 开关,0:关(拒绝服务并给出提示),1:开(放行)
    String switchVal() default "0";

    // 提示信息,默认值可在使用注解时自行定义。
    String message() default "当前请求人数过多,请稍后重试。";
}

5、AOP核心实现

核心实现中我专门加了详细的注释说明,保证大家一看就懂,而且把查询开关的方式列举出来供大家自己选择。

package com.wang.aop;

import com.wang.annotation.ServiceSwitch;
import com.wang.constant.Constant;
import com.wang.exception.BusinessException;
import com.wang.utils.Result;
import lombok.AllArgsConstructor;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @projectName: spring-study
 * @package: com.wang.aop
 * @className: ServiceSwitchAOP
 * @author: wangwujie
 * @description: TODO
 * @date: 2024-1-18 10:32
 */

@Aspect
@Component
@Slf4j
@AllArgsConstructor
public class ServiceSwitchAOP {

    private final StringRedisTemplate redisTemplate;

    /**
     * 定义切点,使用了@ServiceSwitch注解的类或方法都拦截
     */
    @Pointcut("@annotation(com.wang.annotation.ServiceSwitch)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point) {

        // 获取被代理的方法的参数
        Object[] args = point.getArgs();
        // 获取被代理的对象
        Object target = point.getTarget();
        // 获取通知签名
        MethodSignature signature = (MethodSignature) point.getSignature();

        try {

            // 获取被代理的方法
            Method method = target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
            // 获取方法上的注解
            ServiceSwitch annotation = method.getAnnotation(ServiceSwitch.class);

            // 核心业务逻辑
            if (annotation != null) {

                String switchKey = annotation.switchKey();
                String switchVal = annotation.switchVal();
                String message = annotation.message();

            /*
              获取配置项说明
              这里有两种方式:1、配置加在Redis,查询时从Redis获取;
                          2、配置加在数据库,查询时从表获取。(MySQL单表查询其实很快,配置表其实也没多少数据)
              我在工作中的做法:直接放到数据库,但是获取配置项的方法用SpringCache缓存,
                           然后在后台管理中操作配置项,变更时清理缓存即可。
                           我这么做就是结合了上面两种各自的优点,因为项目中配置一般都是用后台管理来操作的,
                           查表当然更舒适,同时加上缓存提高查询性能。
             */

                // 下面这块查询配置项,大家可以自行接入并修改。
                // 数据库这么查询:String configVal = systemConfigService.getConfigByKey(switchKey);
                // 这里我直接从redis中取,使用中大家可以按照意愿自行修改。
                String configVal = redisTemplate.opsForValue().get(Constant.ConfigCode.REG_PAY_SWITCH);
                if (switchVal.equals(configVal)) {
                    // 开关打开,则返回提示。
                    return new Result<>(HttpStatus.FORBIDDEN.value(), message);
                }
            }

            // 放行
            return point.proceed(args);

        } catch (Throwable e) {
            throw new BusinessException(e.getMessage(), e.getMessage());
        }
    }
}

6、定义常量

主要用来存放各种开关的key

package com.wang.constant;

/**
 * @projectName: spring-study
 * @package: com.wang.constant
 * @className: Constant
 * @author: wangwujie
 * @description: TODO
 * @date: 2024-1-18 10:31
 */

public class Constant {

    // .... 其他业务相关的常量 ....

    // 配置相关的常量
    public static class ConfigCode {

        // 挂号支付开关(0:关,1:开)
        public static final String REG_PAY_SWITCH = "reg_pay_switch";
        // 门诊支付开关(0:关,1:开)
        public static final String CLINIC_PAY_SWITCH = "clinic_pay_switch";

        // 其他业务相关的配置常量
        // ....
    }
}

7、使用注解

我们定义一个服务来使用这个开关,我设定了一个场景是挂号下单,也就是把开关用在支付业务这里。

因为支付场景在线上有可能出现未知问题,比如第三方rpc调用超时或不响应,或者对方业务出现缺陷,导致我方不断出现长款,那么我们此时立马操作后台将支付开关关掉,能最大程度止损。

package com.wang.service;

import com.wang.annotation.ServiceSwitch;
import com.wang.constant.Constant;
import com.wang.utils.Result;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

/**
 * @projectName: spring-study
 * @package: com.wang.service
 * @className: RegService
 * @author: wangwujie
 * @description: TODO
 * @date: 2024-1-18 10:32
 */
@Service
public class RegService {

    /**
     * 挂号下单
     */
    @ServiceSwitch(switchKey = Constant.ConfigCode.REG_PAY_SWITCH)
    public Result createOrder() {

        // 具体下单业务逻辑省略....

        return new Result(HttpStatus.OK.value(), "挂号下单成功");
    }
}

8、统一返回类Result

设置统一请求返回Result,以及ResultCode枚举类,用来处理统一返回请求参数。

package com.wang.utils;

/**
 * @projectName: spring-study
 * @package: com.wang.utils
 * @className: ResultCode
 * @author: wangwujie
 * @description: TODO
 * @date: 2024-1-18 10:46
 */
public enum ResultCode {
    SUCCESS(0, "Success"),
    ERROR(1, "Error");
    // 其他状态码...

    private final int code;
    private final String message;

    ResultCode(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
    }

package com.wang.utils;

import lombok.Data;

/**
 * @projectName: spring-study
 * @package: com.wang.utils
 * @className: Result
 * @author: wangwujie
 * @description: TODO
 * @date: 2024-1-18 10:34
 */
@Data
public class Result<T> {
    private int code;
    private String message;
    private T data;

    public Result(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public Result(int code, String message,T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    // 构造方法,getter和setter省略

    public static <T> Result<T> success() {
        return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage());
    }

    public static <T> Result<T> success(T data) {
        return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
    }

    public static <T> Result<T> error(ResultCode resultCode) {
        return new Result<>(resultCode.getCode(), resultCode.getMessage());
    }

    public static <T> Result<T> error(ResultCode resultCode, String message) {
        return new Result<>(resultCode.getCode(), message);
    }
}

9、全局异常处理

设置全局异常BusinessException类,以及全局异常处理类GlobalExceptionHandler。

package com.wang.exception;

/**
 * @projectName: spring-study
 * @package: com.wang.exception
 * @className: BusinessException
 * @author: wangwujie
 * @description: TODO
 * @date: 2024-1-18 14:26
 */
public class BusinessException extends RuntimeException {

    private final String code;

    public BusinessException(String code, String message) {
        super(message);
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}

package com.wang.exception;

/**
 * @projectName: spring-study
 * @package: com.wang.exception
 * @className: GlobalExceptionHandler
 * @author: wangwujie
 * @description: TODO
 * @date: 2024-1-18 14:22
 */
import com.wang.utils.Result;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseEntity<Result<Object>> handleBusinessException(BusinessException ex) {
        Result<Object> result = new Result<>(Integer.getInteger(ex.getCode()), ex.getMessage(), null);
        return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST);
    }

    // 可以添加其他异常处理方法...

}

8、测试效果-controller

好了,接下来我们定义一个接口来测试效果如何。

package com.wang.controller;

/**
 * @projectName: spring-study
 * @package: com.wang.controller
 * @className: RegController
 * @author: wangwujie
 * @description: TODO
 * @date: 2024-1-18 10:33
 */
import com.wang.service.RegService;
import com.wang.utils.Result;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
@AllArgsConstructor
public class RegController {

    private final RegService regService;

    @GetMapping("/createOrder")
    public Result createOrder() {

        return regService.createOrder();
    }
}

Redis中把开关加上去(实际工作中是后台添加的哈),此时开关是1,表示开关打开。

调接口,可以发现,目前是正常的业务流程。

接下来,我们假定线上出了问题,要立马将开关关闭。(还是操作Redis,实际工作中是后台直接关掉哈)

我们将其改为0,也就是表示开关给关闭。

这里要记住一点,提示可以自定义,但是不要直接返回给用户系统异常,给一个友好提示即可。

总结

文中使用到的技术主要是这些:SpringBoot、自定义注解、AOP、Redis、Lombok。

其中,自定义注解和AOP是核心实现,Redis是可选项,你也可以接入到数据库。

lombok的话大家可以仔细看代码,我用它帮助省略了所有@Autowaird,这样就使用了官方及IDEA推荐的构造器注入方式。

好了,今天的小案例,xdm学会了吗。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1395593.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

编译FFmpeg4.3.1 、x264并移植到Android

1、前言 FFmpeg 既是一款音视频编解码工具&#xff0c;同时也是一组音视频编解码开发套件。 2、准备工作 系统&#xff1a;LinuxNDK&#xff1a;android-ndk-r21b-linux-x86_64.zipFFmpeg&#xff1a;ffmpeg-snapshot.tar.bz2x264&#xff1a;x264 3、下载NDK 在linux环境中…

触摸按键控制LED灯

目录 1.理论 2.代码 2.1 touch_ctrl_led.v 2.2 tb_touch_ctrl_led 1.理论 以上的波形图的touch_flag是采用组合逻辑的方式产生的。 以上的touch_flag是采用时序逻辑产生的&#xff0c;时序逻辑会延迟一拍。 以上是上升沿和下降沿的组合逻辑和时序逻辑实现&#xff0c;逻辑或…

Spring Boot - 利用Resilience4j-Circuitbreaker实现断路器模式_防止级联故障

文章目录 PreResilience4j概述Resilience4j官方地址Resilience4j-Circuitbreaker应用场景微服务演示Address servicePOMModelRepositoryServiceControllerData InitProperties测试 Order serviceModelRepositoryServiceSet UpProperties测试 探究断路器调用order-service API 2…

spring boot学习第八篇:kafka监听消费

为了实现监听器功能 pom.xml文件内容如下&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLoc…

.NetCore Flurl.Http 4.0.0 以上管理客户端

参考原文地址&#xff1a;Managing Clients - Flurl 管理客户端 Flurl.Http 构建在堆栈之上System.Net.Http。如果您熟悉HttpClient&#xff0c;那么您可能听说过这个建议&#xff1a;不要为每个请求创建一个新客户端&#xff1b;重复使用它们&#xff0c;否则将面临后…

navigateTo失效-跳转不了页面解决办法!uniapp\vue

改了一个小时多的错误&#xff0c;跳转页面无论怎么样都跳转不了&#xff0c;有2个问题&#xff1a; 注意&#xff1a;uniapp的报错可以在console里检查&#xff01; 1.pages.json文件没有配置路径&#xff0c; 在pages:[ ]里面加 &#xff08;根据自己的路径进行修改 {&qu…

Plane Geometry (Junior High School)

初中平面几何&#xff0c; ACBD, ∠CAD60&#xff0c;∠C40&#xff0c;求∠B Vertical Calculation-CSDN博客 Rectangular Area-CSDN博客

详细介绍IP 地址、网络号和主机号、ABC三类、ip地址可分配问题、子网掩码、子网划分

1、 IP 地址: 网络之间互连的协议&#xff0c;是由4个字节(32位二进制)组成的逻辑上的地址。 将32位二进制进行分组&#xff0c;分成4组&#xff0c;每组8位(1个字节)。【ip地址通常使用十进制表示】ip地址分成四组之后&#xff0c;在逻辑上&#xff0c;分成网络号和主机号 2…

Java多线程并发篇----第二十二篇

系列文章目录 文章目录 系列文章目录前言一、DelayQueue(缓存失效、定时任务 )二、SynchronousQueue(不存储数据、可用于传递数据)三、LinkedTransferQueue四、LinkedBlockingDeque前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家…

港科夜闻|香港科大团队研发多功能,可重构和抗破坏单线感测器阵列

关注并星标 每周阅读港科夜闻 建立新视野 开启新思维 1、香港科大团队研发多功能、可重构和抗破坏单线感测器阵列。研究人员开发出一种受人类听觉系统启发的感测器阵列设计技术。透过模仿人耳根据音位分布来区分声音的能力&#xff0c;这种新型感测器阵列方法可能优化感测器阵列…

Ansible-基础模块

目录 一 Ansible概述 1.Ansible是什么 2.为什么要学Ansible&#xff08;特点&#xff09; 二 Ansible的架构及运行机制 1.Ansible的架构 2.Ansible的运行机制 三 Ansible环境安装部署 1.控制节点安装ansible 2.Ansible目录结构 3.Ansible 命令行模块 &#xff08;1&a…

强化学习(二)多臂老虎机 “Multi-armed Bandits”——1

将强化学习与机器学习、深度学习区分开的最重要的特征为&#xff1a;它通过训练中信息来评估所采取的动作&#xff0c;而不是给出正确的动作进行指导&#xff0c;这极大地促进了寻找更优动作的需求。 1、多臂老虎机&#xff08;Multi-armed Bandits&#xff09;问题 赌场的老虎…

解析Transformer模型

原文地址&#xff1a;https://zhanghan.xyz/posts/17281/ 进入Transformer RNN很难处理冗长的文本序列&#xff0c;且很容易受到所谓梯度消失/爆炸的问题。RNN是按顺序处理单词的&#xff0c;所以很难并行化。 用一句话总结Transformer&#xff1a;当一个扩展性极佳的模型和一…

全链路压力测试:现代软件工程中的重要性

全链路压力测试不仅可以确保系统在高负载下的性能和稳定性&#xff0c;还能帮助企业进行有效的风险管理和性能优化。在快速发展的互联网时代&#xff0c;全链路压力测试已成为确保软件产品质量的关键步骤。 1、测试环境搭建 测试应在与生产环境尽可能相似的环境中进行&#xff…

RabbitMQ 部署与配置[CentOS7]

# RabbitMQ,Erlang 版本包对应 https://rabbitmq.com/which-erlang.html#eol-seriescd /usr/local/src# Erlang下载 # https://github.com/rabbitmq/erlang-rpm/releases https://github.com/rabbitmq/erlang-rpm/releases/download/v23.3.4.5/erlang-23.3.4.5-1.el7.x86_64.rp…

Spring 核心之 IOC 容器学习一

IOC 与 DI IOC(Inversion of Control)控制反转&#xff1a;所谓控制反转&#xff0c;就是把原先我们代码里面需要实现的对象创建、依赖的代码&#xff0c;反转给容器来帮忙实现。那么必然的我们需要创建一个容器&#xff0c;同时需要一种描述来让容器知道需要创建的对象与对象…

【探索C++容器:vector的使用和模拟实现】

【本节目标】 1.vector的介绍及使用 2.vector深度剖析及模拟实现 1.vector的介绍及使用 1.1 vector的介绍 vertor文档介绍 1. vector是表示可变大小数组的序列容器。2. 就像数组一样&#xff0c;vector也采用连续存储空间来存储元素。也就是意味着可以采用下标对vector的元…

时间触发以太网TTE交换机混合流量测试方法

在本世纪初&#xff0c;TTE最早是由维也纳科技大学Hermann Kopetz 赫尔曼科佩茨教授等人提出来的&#xff0c;在国际上比较知名的TTE开发机构主要是以奥地利的TTTech公司为主&#xff0c;尔曼科佩茨教授是该公司的创始人之一&#xff0c;这家公司是将教授的理论进行了产业化应用…

iOS原生应用屏幕适配完整流程

1. 已iPhone 11 布局为设计布局,其他机型已这个来适配 2.变量与控件对应关系 txtViewer: txtAccount txtpwd seg btnOk 3.适配方法实现: //iOS屏幕适配 -(vo

深度求索开源国内首个 MoE 大模型 | DeepSeekMoE:在专家混合语言模型中实现终极专家专业化

文章目录 一、前言二、主要内容三、总结 &#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 一、前言 在大语言模型时代&#xff0c;混合专家模型&#xff08;MoE&#xff09;是一种很有前途的架构&#xff0c;用于在扩展模型参数时管理计算成本。然而&a…