springboot源码剖析-自定义Start实现接口频率控制@RepeatSubmit

目录

SpringBoot starter机制

为什么要自定义starter

自定义starter的案例

自定义starter的命名规则

自定义starter代码实现

效果展示


SpringBoot starter机制

SpringBoot中的starter是一种非常重要的机制,能够抛弃以前繁杂的配置,将其统一集成进 starter,应用者只需要在maven中引入starter依赖,SpringBoot就能自动扫描到要加载的信息并 启动相应的默认配置。starter让我们摆脱了各种依赖库的处理,需要配置各种信息的困扰。 SpringBoot会自动通过classpath路径下的类发现需要的Bean,并注册进IOC容器。SpringBoot提 供了针对日常企业应用研发各种场景的spring-boot-starter依赖模块。所有这些依赖模块都遵循着 约定成俗的默认配置,并允许我们调整这些配置,即遵循“约定大于配置”的理念。

简而言之,starter就是一个外部的项目,我们需要使用它的时候就可以在当前springboot项目中 引入它。

为什么要自定义starter

在我们的日常开发工作中,经常会有一些独立于业务之外的配置模块,我们经常将其放到一个特定 的包下,然后如果另一个工程需要复用这块功能的时候,需要将代码硬拷贝到另一个工程,重新集 成一遍,麻烦至极。如果我们将这些可独立于业务代码之外的功配置模块封装成一个个starter, 复用的时候只需要将其在pom中引用依赖即可,再由SpringBoot为我们完成自动装配,就非常轻松了。

自定义starter的案例

自定义starter的命名规则

SpringBoot提供的starter以 spring-boot-starter-xxx 的方式命名的。

官方建议自定义的starter使用 xxx-spring-boot-starter 命名规则。以区分SpringBoot生态提供 的starter

自定义starter代码实现

给出一个需求:

同一个ip规定秒数内不能重复点击某一接口。

比如10秒内只能点击一次下单接口,否则返回频繁请求。不处理用户的请求,通过注解的方式实现。

分析:

1.首先我们肯定需要自定义一个注解 让其可以标注在方法上。并且能够自主设置请求间隔时间,

2.然后可以自定义校验规则,不局限于只是根据ip校验,比如可以扩展为按照登陆用户来校验。因为在测试的时候 大家都在同一ip下进行测试,如果只是检验ip就无法测试了。这里利用redis自动过期功能。

3.如何在不改变源码的前提下加上这个限制了,肯定是用aop的前置通知塞。在进入方法之前进行检验。

4.自动注入。通过spring boot源码分析时可以知道springboot会默认去加载resources 包下的META-INF文件夹下的spring.factories文件org.springframework.boot.autoconfigure.EnableAutoConfiguration中的所有类全部加载进ioc(满足条件的)。

(1)自定义starter

首先,先完成自定义starter

(1)新建maven jar工程,工程名为repeated-submit-spring-boot-starter,导入最重要的依赖

<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-autoconfigure</artifactId>
			<version>2.2.9.RELEASE</version>
		</dependency>
	</dependencies>

完整的导入


	<dependencies>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<version>2.5.1</version>
		</dependency>

		<dependency>
			<groupId>cn.hutool</groupId>
			<artifactId>hutool-all</artifactId>
			<version>5.4.0</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
			<version>2.5.1</version>
			<!-- 排除lettuce包,使用 jedis 代替-->
			<exclusions>
				<exclusion>
					<groupId>io.lettuce</groupId>
					<artifactId>lettuce-core</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>2.9.0</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-autoconfigure</artifactId>
			<version>2.2.9.RELEASE</version>
		</dependency>

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


		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.20</version>
			<scope>compile</scope>
		</dependency>
	</dependencies>

(2)编写自定义注解

/**
 * 不能重复提交标签
 *
 * @author Administrator
 * @since: 2021/3/16 14:35
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {

    /**
     * 指定时间内,不能重复提交
     *
     * @return 超时时间,单位毫秒
     */
    long timeout() default 5000;

    /**
     * key生成方式
     *
     * @return 结果
     */
    Class<? extends KeyGenerator> keyGenerator() default RedisKeyGenerator.class;

}

timeout 自定义间隔时间,keyGenerator自定义校验规则 也就是redis的key的生成规则。

​​​​​(3)KeyGenerator生成key的接口类

/**
 * @author Administrator
 * @since: 2021/3/16 15:09
 */
public interface KeyGenerator {

    /**
     * 生成key
     *
     * @param request 请求
     * @param method  接口方法
     * @return 结果
     */
    String generate(HttpServletRequest request, Method method);

}

(4)KeyGenerator生成key的接口类的默认实现类RedisKeyGenerator

/**
 * @author Administrator
 * @since: 2021/3/16 15:23
 */
public class RedisKeyGenerator implements KeyGenerator {

    private static final String TEMPLATE = "{}_{}";


    @Override
    public String generate(HttpServletRequest request, Method method) {

    	// 当前请求的ip
        String ip = ServletUtil.getClientIP(request);
        // 请求的类的全路径
        String className = method.getDeclaringClass().getName();
        // 请求方法的名称
        String methodName = method.getName();

        String signature = StrUtil.format(TEMPLATE, className, methodName);
        int hashCode = Math.abs(signature.hashCode());

        return StrUtil.format(TEMPLATE, ip, hashCode);
    }

}
(4)RepeatSubmitAspect aop实现类
/**
 * @author Administrator
 * @since: 2021/3/16 14:40
 */
@Aspect
@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class RepeatSubmitAspect {

    private final Checker checker;


    @Around("@annotation(com.my.starter.resubmit.core.annotation.RepeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取request对象
        HttpServletRequest request = WebUtils.getRequest();

        // 获取方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        log.debug("==> 判断接口重复提交开始");
        Result<Void> checkResult = checker.check(request, method).fallback();
        if (!checkResult.isSuccess()) {
            return checkResult;
        }
        log.debug("==> 判断接口重复提交结束");

        try {
            return joinPoint.proceed();
        } catch (Exception e) {
            log.error("==> 判断接口重复提交出错:", e);
            checker.remove(request, method);
            throw e;
        }
    }

}

直接利用环绕通知去扫描具有自定义注解的方法,当请求来临时就会先进这个前置方法。

(5)Checker校验接口以及其实现类RepeatSubmitChecker里面进频率检验

/**
 * @author Administrator
 * @since: 2021/3/16 14:55
 */
public interface Checker {

    /**
     * 检查接口能否提交
     *
     * @param request 请求
     * @param method  接口方法
     * @return 结果
     */
    Result<Void> check(HttpServletRequest request, Method method);

    /**
     * 移除限制
     *
     * @param request 请求
     * @param method  接口方法
     */
    void remove(HttpServletRequest request, Method method);
}
/**
 * @author Administrator
 * @since: 2021/3/16 15:08
 */
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class RepeatSubmitChecker implements Checker {

    private final RedisTemplate<String, Object> redisTemplate;

    @Override
    public Result<Void> check(HttpServletRequest request, Method method) {
        RepeatSubmit repeatSubmit = method.getAnnotation(RepeatSubmit.class);

        KeyGenerator keyGenerator = ReflectUtil.newInstance(repeatSubmit.keyGenerator());
        String key = keyGenerator.generate(request, method);

        Object value = redisTemplate.opsForValue().get(key);
        if (Objects.nonNull(value)) {
            return Result.error("您的操作过频繁,请稍后再试");
        }

        long timeout = repeatSubmit.timeout();
        if (timeout < 0) {
            timeout = 5000;
        }

        redisTemplate.opsForValue().set(key, Math.abs(key.hashCode()), timeout, TimeUnit.MILLISECONDS);

        return Result.success();
    }

    @Override
    public void remove(HttpServletRequest request, Method method) {
        RepeatSubmit repeatSubmit = method.getAnnotation(RepeatSubmit.class);
        KeyGenerator keyGenerator = ReflectUtil.newInstance(repeatSubmit.keyGenerator());
        String key = keyGenerator.generate(request, method);
        redisTemplate.delete(key);
    }

}

(6)RepeatSubmitConfiguration注入配置类。

/**
 * @author Administrator
 * @since: 2021/3/16 15:40
 */
@ConditionalOnClass(Jedis.class)
@Configuration
public class RepeatSubmitConfiguration {

	@Bean
	@ConditionalOnMissingBean(Checker.class)
	public Checker checker(RedisTemplate redisTemplate) {
		return new RepeatSubmitChecker(redisTemplate);
	}

	@Bean
	public RepeatSubmitAspect repeatSubmitAspect(Checker checker) {
		return new RepeatSubmitAspect(checker);
	}

}

(7)spring.factories编写 将我们定义的配置类的全路径编写进去,springboot加载的时候就能帮我们加载进ioc中。

### 自动配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.my.starter.resubmit.autoconfigure.RepeatSubmitConfiguration

效果展示

导入我们写的starter  记得配置redis的连接哦

	<dependency>
			<groupId>com.my.starter.resubmit</groupId>
			<artifactId>zdy-spring-boot-starter</artifactId>
			<version>1.0-SNAPSHOT</version>
		</dependency>

源码地址

文章内容输出来源:拉勾教育Java高薪训练营;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

起风了 收衣服

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值