SpringBoot-Starter 造轮子之自动锁组件

4cf008b4ecc2b6507376b702d4cdd9a5.jpeg来源:juejin.cn/post/7229615985807982649

👉 欢迎加入小哈的星球 ,你将获得: 专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/  赠书福利

全栈前后端分离博客项目 1.0 版本完结啦,2.0 正在更新中..., 演示链接http://116.62.199.48/ ,全程手摸手,后端 + 前端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,直到项目上线。目前已更新了189小节,累计30w+字,讲解图:1308张,还在持续爆肝中.. 后续还会上新更多项目,目标是将Java领域典型的项目都整一波,如秒杀系统, 在线商城, IM即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,已有780+小伙伴加入(早鸟价超低)

728eeefda7cf898af815db85f43ba836.gif

前言

可能有人会有疑问,为什么外面已经有更好的组件,为什么还要重复的造轮子,只能说,别人的永远是别人的,自己不去造一下,就只能知其然,而不知其所以然。(其实就为了卷)

在日常业务开发的过程中,我们经常会遇到存在高并发的场景,这个时候都会选择使用redis来实现一个锁,来防止并发。

但是很多时候,我们可能业务完成后,就需要把锁释放掉,给下一个线程用,但是如果我们忘记了释放锁,可能就会存在死锁的问题。(对于使用锁不太熟练的话,这种情况时常发生,虽然很多时候,我们的锁是有过期时间的,但是如果忘记了释放,那么在这个过期时间内,还是会存在大的损失)。

还有一点就是,在我们使用redis实现一个锁的时候,我们需要导入redisClient,设置key,设置过期时间,设置是否锁等等一些重复的操作。前面的哪些步骤,很多都是重复的,所以我们可以想一个方法,来把重复的东西都抽象出来,做成统一的处理,同时哪些变化的值,提供一个设置的入口。

抽出来的东西,我们还可以封装成一个spring-boot-stater,这样我们只需要写一份,就可以在不同的项目中使用了。说干就干,下面我们使用redisson,完成一个自动锁的starter

实现

首先,我们分析一下哪些东西是我们需要进行合并,哪些又是需要提供给使用方的。得到下面的一些问题

  • 加锁、释放锁过程 我们需要合并起来

  • 锁key,加锁时间......这些需要给使用方注入

  • 锁的key该怎么去生成(很多时候,我们需要根据业务字段去构造一个key,比如 user:{userId}),那么这个userId该怎么获取?

我们从上面需要解决的问题,去思考需要怎么去实现。我们需要封装一些公共的逻辑,又需要提供一些配置的入库,这样的话,我们可以尝试一种方法,使用 注解+AOP,通过注解的方式完成加锁、解锁。(很多时候,如果需要抽出一些公共的方法,会用到注解+AOP去实现)

定义注解

AutoLock 注解

一个锁需要有的信息有,key,加锁的时间,时间单位,是否尝试加锁,加锁等待时间 等等。(如果还有其他的业务需要,可以添加一个扩展内容,自己去解析处理) 那么这个注解的属性就可以知道有哪些了

/**
 * 锁的基本信息
 */
@Target({ElementType.METHOD})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoLock {

    /**
     * 锁前缀
     */
    String prefix() default "anoxia:lock";

    /**
     * 加锁时间
     */
    long lockTime() default 30;

    /**
     * 是否尝试加锁
     */
    boolean tryLock() default true;

    /**
     * 等待时间,-1 不等待
     */
    long waitTime() default -1;

    /**
     * 锁时间类型
     */
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;

}
LockField 注解

这个注解添加到参数属性上面,用来解决上面提到获取不同的业务参数内容构造key的问题。所以我们需要提供一个获取哪些字段来构造这个key配置,这里需要考虑两个问题:

  • 1、参数是基本类型

  • 2、参数是引用类型 - 这种类型需要从对象中拿到对象的属性值

/**
 * 构建锁的业务数据
 * @author huangle
 * @date 2023/5/5 15:01
 */
@Target({ElementType.PARAMETER})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface LockField {

    String[] fieldNames() default {};

}

定义切面

重点就在这个切面里面,我们需要在这里完成key的合成,锁的获取与释放。整个过程可以分为以下几步

  • 获取锁的基本信息,构建key

  • 加锁,执行业务

  • 业务完成,释放锁

/**
 * 自动锁切面
 * 处理加锁解锁逻辑
 *
 * @author huangle
 * @date 2023/5/5 14:50
 */
@Aspect
@Component
public class AutoLockAspect {

    private final static Logger LOGGER = LoggerFactory.getLogger(AutoLockAspect.class);

    @Resource
    private RedissonClient redissonClient;

    private static final String REDIS_LOCK_PREFIX = "anoxiaLock";

    private static final String SEPARATOR = ":";


    /**
     * 定义切点
     */
    @Pointcut("@annotation(cn.anoxia.lock.annotation.AutoLock)")
    public void lockPoincut() {
    }


    /**
     * 定义拦截处理方式
     *
     * @return
     */
    @Around("lockPoincut()")
    public Object doLock(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取需要加锁的方法
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        // 获取锁注解
        AutoLock autoLock = method.getAnnotation(AutoLock.class);
        // 获取锁前缀
        String prefix = autoLock.prefix();
        // 获取方法参数
        Parameter[] parameters = method.getParameters();
        StringBuilder lockKeyStr = new StringBuilder(prefix);
        Object[] args = joinPoint.getArgs();
        // 遍历参数
        int index = -1;
        LockField lockField;
        // 构建key
        for (Parameter parameter : parameters) {
            Object arg = args[++index];
            lockField = parameter.getAnnotation(LockField.class);
            if (lockField == null) {
                continue;
            }
            String[] fieldNames = lockField.fieldNames();
            if (fieldNames == null || fieldNames.length == 0) {
                lockKeyStr.append(SEPARATOR).append(arg);
            } else {
                List<Object> filedValues = ReflectionUtil.getFiledValues(parameter.getType(), arg, fieldNames);
                for (Object value : filedValues) {
                    lockKeyStr.append(SEPARATOR).append(value);
                }
            }
        }
        String lockKey = REDIS_LOCK_PREFIX + SEPARATOR + lockKeyStr;
        RLock lock = redissonClient.getLock(lockKey);
        // 加锁标志位
        boolean lockFlag = false;
        try {
            long lockTime = autoLock.lockTime();
            long waitTime = autoLock.waitTime();
            TimeUnit timeUnit = autoLock.timeUnit();
            boolean tryLock = autoLock.tryLock();
            try {
                if (tryLock) {
                    lockFlag = lock.tryLock(waitTime, lockTime, timeUnit);
                } else {
                    lock.lock(lockTime, timeUnit);
                    lockFlag = true;
                }
            }catch (Exception e){
                LOGGER.error("加锁失败!,错误信息", e);
                throw new RuntimeException("加锁失败!");
            }
            if (!lockFlag) {
                throw new RuntimeException("加锁失败!");
            }
            // 执行业务
            return joinPoint.proceed();
        } finally {
            // 释放锁
            if (lockFlag) {
                lock.unlock();
                LOGGER.info("释放锁完成,key:{}",lockKey);
            }
        }
    }


}
获取业务属性

这个是一个获取对象中字段的工具类,在一些常用的工具类里面也有实现,可以直接使用也可以自己实现一个

/**
 * @author huangle
 * @date 2023/5/5 15:17
 */
public class ReflectionUtil {

    public static List<Object> getFiledValues(Class<?> type, Object target, String[] fieldNames) throws IllegalAccessException {
        List<Field> fields = getFields(type, fieldNames);
        List<Object> valueList = new ArrayList();
        Iterator fieldIterator = fields.iterator();

        while(fieldIterator.hasNext()) {
            Field field = (Field)fieldIterator.next();
            if (!field.isAccessible()) {
                field.setAccessible(true);
            }

            Object value = field.get(target);
            valueList.add(value);
        }

        return valueList;
    }


    public static List<Field> getFields(Class<?> claszz, String[] fieldNames) {
        if (fieldNames != null && fieldNames.length != 0) {
            List<String> needFieldList = Arrays.asList(fieldNames);
            List<Field> matchFieldList = new ArrayList();
            List<Field> fields = getAllField(claszz);
            Iterator fieldIterator = fields.iterator();

            while(fieldIterator.hasNext()) {
                Field field = (Field)fieldIterator.next();
                if (needFieldList.contains(field.getName())) {
                    matchFieldList.add(field);
                }
            }

            return matchFieldList;
        } else {
            return Collections.EMPTY_LIST;
        }
    }

    public static List<Field> getAllField(Class<?> claszz) {
        if (claszz == null) {
            return Collections.EMPTY_LIST;
        } else {
            List<Field> list = new ArrayList();

            do {
                Field[] array = claszz.getDeclaredFields();
                list.addAll(Arrays.asList(array));
                claszz = claszz.getSuperclass();
            } while(claszz != null && claszz != Object.class);

            return list;
        }
    }
}

配置自动注入

在我们使用 starter 的时候,都是通过这种方式,来告诉spring在加载的时候,完成这个bean的初始化。这个过程基本是定死的。就是编写配置类,如果通过springBoot的EnableAutoConfiguration来完成注入。注入后,我们就可以直接去使用这个封装好的锁了。

/**
 * @author huangle
 * @date 2023/5/5 14:50
 */
@Configuration
public class LockAutoConfig {
    
    @Bean
    public AutoLockAspect autoLockAspect(){
        return new AutoLockAspect();
    }

}

// spring.factories 中内容
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.anoxia.lock.config.LockAutoConfig

测试

我们先打包这个sarter,然后导入到一个项目里面(打包导入的过程就不说了,自己去看一下就可以) 直接上测试类,下面执行后可以看到锁已经完成了释放。如果业务抛出异常导致中断也不用担心锁不会释放的问题,因为我们是在 finally 中释放锁的

/**
 * @author huangle
 * @date 2023/5/5 14:28
 */
@RestController
@RequestMapping("/v1/user")
public class UserController {

    @AutoLock(lockTime = 3, timeUnit = TimeUnit.MINUTES)
    @GetMapping("/getUser")
    public String getUser(@RequestParam @LockField String name) {
        return "hello:"+name;
    }


    @PostMapping("/userInfo")
    @AutoLock(lockTime = 1, timeUnit = TimeUnit.MINUTES)
    public String userInfo(@RequestBody @LockField(fieldNames = {"id", "name"}) UserDto userDto){
        return userDto.getId()+":"+userDto.getName();
    }

}
4a70218f210d87c10f21ec80a7c0726e.jpeg
图片
c690d6fe3e5d82cb1cbbe23b3d97dc34.jpeg
图片

总结

很多时候,一些公共的业务逻辑都可以被抽象出来成为一个独立的组件而存在,我们可以在日常开发过程中,不断的去思考和寻找看哪些可以被抽象出来,哪些可以更加简化一些。然后尝试去抽象出一个组件出来,这样的话不但可以锻炼自己的能力,还可以得到一些很好用的工具,当然自己抽出的组件可以存在问题,但是慢慢的锻炼下来,总会变的越来越好。怎么说呢,尝试去做,能不能做好再说,做不好就一次又一次的去做。

👉 欢迎加入小哈的星球 ,你将获得: 专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/  赠书福利

全栈前后端分离博客项目 1.0 版本完结啦,2.0 正在更新中..., 演示链接http://116.62.199.48/ ,全程手摸手,后端 + 前端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,直到项目上线。目前已更新了189小节,累计30w+字,讲解图:1308张,还在持续爆肝中.. 后续还会上新更多项目,目标是将Java领域典型的项目都整一波,如秒杀系统, 在线商城, IM即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,已有780+小伙伴加入(早鸟价超低)

a8f5a8553b60fd191b47ee91f06c0d5c.gif

 
 

7a96749134b935d069a1230b0282370e.jpeg59d3028c280b33ed5e901bfdaa2df6d4.gif

 
 
 
 
1. 我的私密学习小圈子~
2. 有了这 4 款脚本工具, 再也不怕写烂SQL了!!!
3. 基于 ThreadLocal 实现一个上下文管理组件(附源码)
4. 微服务下必须了解的4种部署策略!
 
 
最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。
PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。
点“在看”支持小哈呀,谢谢啦
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
自定义Spring Boot Starter是一种用于简化Spring Boot应用程序配置和依赖管理的机制。它由几个组件组成,包括starter包、autoconfiguration包和配置文件。 首先,你需要创建一个Maven工程,并创建三个模块。其中,starter包负责引入依赖,autoconfiguration包负责实现装配。在autoconfiguration包中,你需要定义对应的properties类、configuration类和核心业务类。此外,你还需要在autoconfiguration包的/resources/META-INF/目录下添加spring.factories文件,并配置org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.qiejk.demo.springboot.autoconfiguration.DemoAutoConfiguration。这一步是Spring Boot装配的核心,只有添加了这个内容,Spring Boot才会进行自动装配。\[1\] 在父模块的pom文件中,你需要引入Spring Boot的依赖,例如spring-boot-starter-web,以及指定Spring Boot的版本。这样,你的自定义starter模块就可以基于Spring Boot进行开发和使用。\[2\] 最后,你需要创建一个配置文件,用于告诉Spring Boot在启动时扫描该配置文件,并将其中的配置类加载到Spring容器中。这个配置文件的作用是指定自动配置类的位置。\[3\] 通过以上步骤,你就可以创建自定义的Spring Boot Starter,并在应用程序中使用它来简化配置和依赖管理。 #### 引用[.reference_title] - *1* [如何自定义springboot-starter](https://blog.csdn.net/sinat_29434159/article/details/123995794)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [SpringBoot—自定义Starter篇](https://blog.csdn.net/m0_46571920/article/details/122910726)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值