• Spring AI 智能体模式 2 : Anthropic 智能体 Skill

    Spring AI 新增了对 Anthropic 智能体 Skill 的支持 — 这类模块化能力可让 Claude 直接生成实际文件,而非单纯的文本描述。启用该 Skill 后,Claude 能生成可直接下载使用的 Excel 电子表格、PowerPoint 演示文稿、Word 文档和 PDF 文件。 局限性说明 Anthropic Skill 的实现方案仅适用于其自研的 Claude 系列模型,存在以下局限性: 无移植性: 该 Skill 依赖 Anthropic 的代码执行能力和 Files API 基础设施,无法在其他大模型平台(OpenAI、 Gemini 等)使用; 专属类依赖: 需使用 AnthropicChatOptions 、 AnthropicSkill 、 AnthropicSkillsResponseHelper 等 Anthropic 专属类,而非 Spring AI 的通用接口; 模型限制: 仅 Claude Sonnet 4、Sonnet 4.5 和 Opus 4 模型支持该 Skill ; 文件有效期: 通过 Anthropic Files API 生成的文件,24 小时后会自动过期; 公测功能属性: Skill API 需携带公测版本请求头,且接口规范仍可能持续迭代。 Anthropic Skill 与通用智能体 Skill 的选型建议 Spring AI 支持两种不同的智能体 Skill 实现方案,可根据业务需求选择: 选择 Anthropic 原生Skill API 的场景 需使用预构建的文档生成能力,支持 Excel、PowerPoint、Word、PDF 等格式; 希望 Skill 在沙箱化的安全云端环境中运行; 需实现团队共享的、工作空间级别的 Skill; 业务已确定基于 Claude 系列模型开发; 希望由 Anthropic 负责管理 Skill 的执行基础设施 选择通用智能体 Skill (spring-ai-agent-utils) 的场景 需要让 Skill 适配多款大模型(OpenAI、 Anthropic、 Gemini 等); Skill 需要访问本地资源、网络或自定义软件包; 希望将 Skill 与应用代码一起打包,进行版本控制; 需要对 Skill 的执行环境拥有更高的控制权; 优先考虑方案的可移植性,避免厂商锁定。 能否同时使用两种方案? 可以。在同一应用中,可通过 Anthropic 原生 Skill 实现文档生成,同时借助通用智能体 Skill 完成其他可移植的业务能力开发。二者定位不同、功能互补,可协同使用。

    [阅读更多...]
  • 基于生成式注解为类添加toString方法

    在组内讨论时,有同事提建议在把对象写到日志中时最好直接输出对象不要做任何加工,也就是尽量调用对象自己的toString()方法,不要用JsonKit.toJson(obj)这样先把对象转为json字符串再输出的写法。 这个建议不是没有道理的,jackson和fastjson这些json工具将对象序列化为字符串时会有一个自动检查推测的过程,在这个过程中做了如下事情: 所有public方法,带返回值,符合“getXxx”(或“isXxx”,如果返回boolean会被称为“isgetter”)命名约定的成员方法被推测存在名字为“xxx”的属性(属性名按照bean命名约定推测,即开头大写字母转成小写)。 所有public成员字段被推测为要显示的属性,使用字段名字来序列化。 也就是说,一个“getXxx()”方法中如果做了业务性的处理,在被调用的过程中也会被执行。如下面的伪代码: 在对FetchUserAction的实例进行序列化时,会得到下面的json: 得到这个json的时候意味着至少已经做了 “从应用上下文获得当前用户ID” 和 “从数据库查询用户信息” 两个动作。在输出日志的时候静默的执行了一次涉及到资源的操作,这不是一个合理的事情。 当然也可以为getCurrentUser()这个方法添加类似@JsonIgnore这样的注解以避免出现上面的情况。但问题不在这里,问题的关键在于我们应该只需要对model类的实例或其对应的集合做toJson的处理;其它的执行业务处理的对象不应该被输出到日志中,即使因为种种原因不得不将之输出到日志中也不应该做toJson的处理,以避免出现类似前面的例子中的情况。 如果model类的toString()方法的返回值就已经是经过json序列化的就好了,这样我们在输出日志时就不需要显式地再做这个toJson的操作了,也就不会误将不需要json序列化的对象给序列化了。我一开始想的是通过lombok来解决这个问题,因为model类一般是依赖lombok来生成toString方法的。不幸的是,经过调研,我发现虽然已经有人在lombok的相关issue里提过类似的问题,但是lombok现阶段还不支持这么做。要想解决就只能自己实现了。 期望实现的效果是能够根据类上的一个注解如@ToJson来在编译期自动生成类的toString方法。方法内容是下面这样的: 在toString方法中调用json序列化工具类实现了将当前对象转为json字符串的操作。 最开始我是想用bytebuddy或asm来做这个事情的,但是使用这些字节码工具的时候需要加上javaagent相关的配置,运维肯定是不允许的。后来我又接触到了生成式注解,感觉这应该是解决这个问题的一个出路。 先来看下什么是生成式注解: 生成式注解处理器是JSR-269中定义的API,该API可以在编译期对代码中的特定注解进行处理,从而影响到前端编译器的工作过程,通过生成式注解处理器可以读取、修改、添加抽象语法树中的任意元素。 “可以修改添加抽象语法树中的任意元素”,这看起来很酷,正是我想要的。 关于如何修改语法树可以参考这篇文档:《Java 中的屠龙之术:如何修改语法树?》。 看下具体是如何实现的吧,在类中添加toString方法的代码如下: 上面这个方法实现了向类中添加 toString 方法的逻辑,注释是我用通义灵码生成的,还是挺精准的。 如通义生成的注释说明, makeToStringBody 负责生成toString 方法的具体逻辑,这个方法的逻辑如下: 核心代码就是这些。其他的代码可以看我这个项目 zhyea / lombok-ext 。 本来还可以展开说说lombok和mapstruct的,但就这样吧! END!!!

    [阅读更多...]
  • 使用Spring AOP实现注解式的分布式锁

    这里简单说一个springboot生态下基于redis实现的分布式锁方案。预期实现的效果是在要加锁的方法上添加一个注解,然后就能根据请求参数得到并加上锁,方法执行完后,也会自动释放锁。这样在实现方法时,开发者就可以只关注业务逻辑,不用考虑加锁解锁相关的事情。务求整个过程的丝滑程度类似Spring的 @Transactional 注解。 这个分布式锁暂时基于redis来实现,用来和redis交互的组件则是spring-redisson。当然,我也考虑过基于mysql来实现,以后有时间了也会写一个mysql的版本。不过不管是redis还是mysql,两者都只是实现分布式锁的一个基础中间件,对整体实现思路没有什么影响。 先来看下这个分布式锁的大致结构图:  图中左侧的redis/redisson/RedisProperties是我们实现分布式锁的基础,它们的作用是不需要多说的;右下角的Business代表了各种业务需求及对应的方法,他们需要使用分布式锁来实现业务处理时的互斥性,这也不用解释;在下面的内容中会详细介绍下其他部分,也就是我们这个分布式锁的主要组成部分。 LockAdvisor LockAdvisor 是分布式锁的基础组件,它由LockPointcut 和 LockAdvice 组成。当然,不只是当前这个分布式锁,spring aop的核心结构一般都是 Advisor,Advisor中又有Pointcut和Advice两个成员,两者作用大致如下图所示: Pointcut发挥作用是在应用启动时Bean实例化的过程中,而Advice发挥作用是在目标方法被调用执行的过程中。 Spring在创建Bean实例时,会用所有的Advisor和Bean实例进行匹配,匹配成功了就会创建相应的代理。这其中,匹配是依赖Pointcut来实现的,创建出的代理要做什么则是由Advice决定的。可以说Advisor是Spring创建代理的一个起点。 spring创建代理的过程可以查看spring中的这个方法:AbstractAutoProxyCreator.wrapIfNecessary() 。 LockPointcut 前面也说了,Pointcut会根据方法定义中的信息决定要拦截哪些方法。在分布式锁这个case里, LockPointcut 会根据方法中是否存在 @RLock注解来完成拦截并创建代理。当然实际情况会比较复杂一点,这里在识别到@RLock注解后又对注解中的参数进行了解析和缓存以便进行复用,所以在实现的时候是继承的StaticMethodMatcherPointcut这个类。如果只是要匹配@RLock注解,完全可以依赖Spring提供的AnnotationMethodMatcher来实现。 在springboot中提供了多种Pointcut和MethodMatcher的实现类,在使用时可以根据自己的情况选择使用spring提供的实现,不必一定要自己造轮子。 LockAdvice 在LockAdvice里记录了在目标方法执行前后获得锁及释放锁的详细过程。在这个分布式锁的实现中,我把LockAdvice分成了LockInterceptor、LockAspectSupport、SpelEvaluator三个部分,三者的关系如下图: 把LockAdvice分成三部分的目的是为了划分职责,也就是遵循单一性原则。三部分的职责分别如下: LockInterceptor 只是简单定义了切面,仅是个入口,具体的锁处理还是需要父类LockAspectSupport来实现; LockAspectSupport 提供了锁能力的支持,在这个类里会完成与redis的通信从而实现锁获得和锁关闭的处理 SpelEvaluator 提供了SpEL解析的能力,分布式锁的key一般不会是一个常量,需要根据方法参数动态组装,SpelEvaluator就提供了根据方法参数动态组装锁key的能力 LockAutoConfiguration LockAutoConfiguration 看起来像是一个配置类,但实际上在这个类里完成的是分布式锁实现中需要的各种Bean的创建。这里我们我们可以直接看下代码: 从代码可以看到,LockAutoConfiguration中确实引入了一个配置类 RedisProperties,这个类对应着项目配置文件(application.yml)中redis的配置。通过@EnableConfigurationProperties(RedisProperties.class)可以获得redis的配置信息,从而创建RedisClient的Bean实例。 前面提到的几个类的实例都会在这里完成创建并最终被Spring得到并管理使用起来。 EnableRLock 前面提到在LockAutoConfiguration中创建了多个Bean,但是这些Bean该怎么注入到Spring容器中呢。 我们在写代码的时候应该都用过@ComponentScan注解,在启动类中通过@ComponentScan注解显示注入LockAutoConfiguration及其中的各种Bean确实是一个办法,但不好不优雅。 刷过springboot面试题的同学应该接触过spring.factories,这是一种springboot推荐的做法。springboot的应用在启动时会扫描所有依赖中的spring.factories文件,并自动注入文件中定义的AutoConfiguration类及类中定义的各种Bean。 在这个分布式锁的实现中采用的是另一种做法,是通过在项目启动类上定义的 @EnableRLock注解及@EnableRLock中的@Import注解注入LockAutoConfiguration及其中的各种Bean。体验上类似开启springboot事务支持能力的@EnbaleTransactional注解。(关于@Import注解可以参考 SpringBoot探索01 – @Import注解 ) 这个分布式锁的具体实现可以看redis上的代码: zhyea / rlock-spring-boot-starter   也可以直接添加如下依赖使用: 就这样。 END!!!

    [阅读更多...]
  • MapStruct属性多转一实现

    在项目里遇到了需要使用mapstruct将source对象的多个属性转为target对象的一个属性的场景。针对这个问题研究了一段时间,发现想要解决得好一些还是挺让人头疼的。 先说结论吧:MapStruct支持将多个对象转为一个对象,但是不支持将多个属性转为一个属性。对,mapstruct是不支持这么做的。 最终的解决方案也非常简单:在使用mapstruct完成对象的简单转换后,再做一次加工就行。不过我想将这个事情做得优雅一些,目的是尽量不影响业务代码。 项目的代码不好拿出来,举个例子来说明下,在下面的代码中定义了一个产品的Entity类及相应的Item类。目标是将Entity类的实例通过mapstruct转为Item类的实例。 先看下类的定义: 产品Entity类 ProductEntity: 产品Item类 ProductItem : ProductItem 比 ProductEntity 多了一个 status 属性,这个status属性可以由 生产日期 (manufactureDate)和保质期(qualityGranteeMonths)计算出来。计算逻辑可以看下 ProductStatusEnum 的定义及类中静态的analyze方法: 要实现基于 ProductEntity 的生产日期和保质期两个字段映射出Item的status的值,可以在完成 Entity和Item的转换后再做一次处理,类似下面的代码: 如上面的代码:在转换接口 ProductConverter 中,定义了一个default方法entity2Item,在这个方法中利用mapstruct生成的 entity2ItemSimply() 方法完成简单转换后又做了一次 生产日期、保质期和状态的映射。因为是在同一个转换接口中定义的,在使用时还是比较丝滑的。 不过有一个小问题,就是现在转换接口 ProductConverter 中存在两个将 Entity转为Item的方法,在处理相关Collection 转换时就会出现因为不知道该调用哪个方法而产生的报错,如下: 要解决这个问题也比较简单,使用 qualifyByName 进行标记即可, 最终代码如下: 对了,不要忘了给 entity2Item() 方法加上 @Name 注解,不然会报相关方法找不到的错的。 就这样。源码在这里: zhy-explore / mapstruct-explore End !!!

    [阅读更多...]
  • springboot入门16 – 包装Controller返回值2

    之前有整理过一次怎样包装SpringBoot Controller的做法。 最近在原有方案的基础上又升级了下,可以通过引入 spring-boot-starter 的形式对接口返回值进行封装。 具体做法如下: 1. 引入 zhy-spring-boot-starter 依赖 同时需要确认已引入 spring-boot-starter-web 依赖。这样 zhy-spring-boot-starter 中的返回值封装组件才会生效。 2. 包装返回值 要封装返回值,只需要在接口类或接口方法上添加@ResponseWrapper注解就可以了。 因为只需要对REST接口进行封装。所以组件只会对 接口类或接口方法 生效。 接口类指存在@RestController注解的类 接口方法指存在@RestController注解的类下的方法,或者存在@ResponseBody方法 对其它的接口封装没有意义,封装后还容易出错,所以加了如上限定。 比如对在 public boolean login(String username, String password) 这样一个登录接口封装后的返回结果大致如: 其中 data 是接口方法的返回值。msg 是存在故障时的异常信息提示。code 是返回结果的状态码,这个状态码可以自定义: 如果要调整接口返回值的code,可以在配置文件 application.yaml 中添加如上配置并做调整。 3. 包装异常信息 组件中提供了默认的异常封装能力。 其中自定义的业务异常需要继承 @RwException 或者直接使用 @RwException 这个异常类。封装后的返回值大致如: 除了业务异常外还对接口参数validator校验异常结果进行了封装。 参数校验失败时的code默认是 10000 。 以上两种异常是可控的业务异常,所以尽管接口返回json中的code值虽然不同,但是接口返回信息中的http status还是200。 此外还有可能会出现一些空指针异常之类的因为编程意外而产生的异常,此时的异常信息统一都是: 而且接口返回信息中的http status是500。 如果还想对异常做更进一步的处理,可以考虑禁用当前组件的异常封装能力,并自定义异常封装能力。 要实现禁用当前组件的异常封装能力可以在配置文件中添加如下配置: 目前就只需要做这些配置就够了。 如果想做些自定义的扩展可以参考源码:github / zhyea / zhy-spring-boot-starter END!!

    [阅读更多...]
  • springboot入门15 – profile设置

    配置方式 命令行方式 命令行方式是一种外部配置的方式,在执行java -jar命令时可以通过 –spring.profiles.active=test的方式进行激活指定的profiles列表。 使用方式如下所示: 系统变量方式 需要添加一个名为SPRING_PROFILES_ACTIVE的环境变量。 linux环境下可以编辑环境变量配置文件/etc/profile,添加下面的一行: windows如何配置就不多说了——不知道就自行百度。 这种方式在docker之类的环境下很有用,一次编译,环境自由切换 Java系统属性方式 Java系统属性方式也是一种外部配置的方式,在执行java -jar命令时可以通过-Dspring.profiles.active=test的方式选择指定的profiles。 使用方式如下所示: 注意:-D 方式设置Java系统属性要在-jar前定义。 配置文件方式 配置文件方式是最常用的方式。我们只需要在application.yml配置文件添加配置即可,使用方式如下所示: 优先级 优先级大致如下: 经过测试命令行方式的优先级最高,而内部配置文件方式则是最低的。 激活多个profile 如果需要激活多个profile可以使用逗号隔开,如: END!!

    [阅读更多...]
  • 基于redis实现分布式bloomfilter

    如果想要判断一个元素是不是在一个集合里,一般想到的是将所有元素保存起来,然后通过比较确定。链表,树等等数据结构都是这种思路. 但是随着集合中元素的增加,我们需要的存储空间越来越大,检索速度也越来越慢(O(n),O(logn))。不过世界上还有一种叫作散列表(又叫哈希表,Hash table)的数据结构。它可以通过一个Hash函数将一个元素映射成一个位阵列(Bit array)中的一个点。这样一来,我们只要看看这个点是不是1就可以知道集合中有没有它了。这就是布隆过滤器的基本思想。 Hash面临的问题就是冲突。假设Hash函数是良好的,如果我们的位阵列长度为m个点,那么如果我们想将冲突率降低到例如 1%, 这个散列表就只能容纳m / 100个元素。显然这就不叫空间效率了(Space-efficient)了。解决方法也简单,就是使用多个Hash,如果检查到一个元素不在集合中,那肯定就不在。如果计算到在集合中,虽然也有一定的错误率,但是错误率还是比较低的。 以上内容来自百度百科《布隆过滤器》词条,阐述了bloomfilter的基础原理。BloomFilter常被用于需要唯一性校验的场景,如爬虫等。 java中有许多bloomfilter的实现包,最常用的还是guava中的BloomFilter。但是现在的应用多存在分布式部署的现象,本地的BloomFilter实现已经不能满足需求了,我们需要一个分布式BloomFilter实现。redis的Bitmap是实现分布式BloomFilter的一个好基础。 首先,我们需要计算一个字符串的不同Hash值的方案,这里准备了一个BloomFilterHelper类: BloomFilterHelper类的构造器需要传入两个参数:expectedInsertions 和 fpp。 expectedInsertions : 预期写入的字符串总数 fpp :误判率 误判率当然是越低越好,但是也意味着要为存储的数据准备更多的空间,但redis的Bitmap天然是有上限的,因此需要根据实际情况做最优设计。 另外,这里使用了commons-codec这个包来计算字符串的hash值。采用的是Murmur hash64计算。 然后是向redis的Bitmap中写入相应信息: 最后是要验证字符串对应的offset是否在Bitmap中。下面是从Bitmap中取值的代码: 以及相应的校验代码: 大体上就是这些内容了。 完整代码在GitHub:zhyea / springboot-redis。 End!

    [阅读更多...]
  • springboot入门14 – Kafka应用

    简述 这几天优化了一下之前写的一个springboot kafka组件。比较起原生的spring-kafka来,我希望能够简化kafka的使用,可以更聚焦于具体的消息处理逻辑。 接下来的内容是这个组件的用法。 使用方法 添加依赖 这个组件已经提交到了maven中央仓库,可以直接通过依赖的形式引入: 0.2.2是这两天刚发布的一个版本。 消费者Processor kafka-spring-boot-starter这个组件已经完成了kafka消费者的主要功能。 对于开发者来说,可以不必关注KafkaConsumer的创建,只需要实现Processor接口并注入到容器中即可。 下面是一个简单的示例: 如示例中,通过@Component注解完成了Processor实现类的实例的注入,并为注入的Bean提供了一个名称:zhyyy。记住这个名称,在之后的配置文件中会用到。 使用生产者 kafka-spring-boot-starter会根据配置主动创建KafkaProducer。开发使用时可以直接从容器中获取ProducerTemplate实例来发送消息: 如果写入kafka的消息的key和value的序列化方案采用的都是默认的字符串(反)序列化方案(StringDeserializer和StringSerializer),可以使用StringProducerTemplate实例: 发送消息时酌情调用不同的send()方法: 配置 下面是一个最简单的配置: 如上配置中: test-group00既是配置项的ID,也是消费组ID bootstrap-servers我想不需要多做解释。 topics对应的是一个数组结构,也可以写作[test-topic1]或[test-topic1,test-topic2],即支持同一个kafka集群上多个类似topic的统一处理 consumer是消费者相关配置,processor对应的是Processor实现类的Bean名称,count标识的是应用内消费线程的数量 默认的序列化方案采用的是字符串序列化方案。 虽然在配置中没有体现,但kafka-spring-boot-starter组件会基于已有的信息创建KafkaProducer,使用时可以通过ProducerTemplate执行消息发送。 比较完整的配置是这样子的: 其中common模块下是一些通用的配置,config模块下则是一或多组具体的配置(这里是两组)。common下的配置会被config下的配置覆盖。 此外还独立出来了一些常用的配置项,如autoOffsetReset,keyDeserializer等,以便在使用时进行配置。 其他 示例应用在 github / spring-boot-kafka。 kafka-spring-boot-starter这个组件的源码也在 github 。如果有定制化的需求可以据此进行调整。 End!

    [阅读更多...]
  • springboot入门13 – 多CacheManager应用

    概述 之前有写过springboot缓存应用的说明(《springboot入门01 – 缓存的使用》)。不过实际的场景有时候会比较复杂一些,比如:需要同时使用redis和caffine来做多级缓存,或者需要在通用配置外应用一些个性化的配置。使用多个CacheManager来分别管理不同的缓存是应对这种问题的一个常规方案。 接下来介绍下如何实现多CacheManager应用。 多CacheManager应用 这里会通过一个具体的案例来进行演示。需求大致是这样的:在应用中的大部分场景都使通用的缓存配置,但是部分特殊场景需要做个性化的配置。在接下来演示中我们主要会用到Caffeine缓存。 配置 先修改下配置。这次如果继续使用SpringBoot的自启动CacheManager会有一些不太好控制的地方,因此不宜再使用默认的缓存配置,需要做些独立配置: 其中 caching.spec 表示通用的缓存配置, caching.special则表示一组需要做个性化配置的特例。 通用配置是一行字符串,读取的时候可以直接使用@Value注解,个性化配置则是一个Map结构,读取的时候需要用到@ConfigurationProperties注解,大致如下: (关于配置文件读取可以参考之前的一篇旧文:《springboot入门07 – 配置文件详解》) 创建CacheManager 接下来就是根据配置文件创建CacheManager了。 创建通用CacheManager直接使用CaffeineCacheManager就可以了: 注意@Primary注解,这个注解表示没有特意注明时,优先选择这个CacheManager。 管理个例的CacheManager略有些麻烦,这里使用了SimpleCacheManager,代码如下: 在使用这个CacheManager时还需要记得下在@CacheConfig或@Cacheable注解中注明对应的qualifier: 至此,多缓存配置已经是没有问题了。详细代码可以参考Git: zhyea / multi-cache 这里区分通用CacheManager与个例CacheManager主要依赖@Primary注解实现。接下来会介绍一些其他的方案。 继承CachingConfigurerSupport 继承抽象类CachingConfigurerSupport后,可以通过实现(重写)cacheManager()方法指明默认的CacheManager。嗯,也就是说,节省了一个@Bean和一个@Primary注解。代码如下: 管理个例的CacheManager还是需要@Bean注解并设置Qualifier的。 实现CacheResolver 这个方案,怎么说呢,应该是可操作性最强大的。如果场景再复杂些,完全可以考虑用这个方案来处理。但是就我们当前这个case来说,用CacheResolver来实现应该是最繁琐的了。太繁琐了,懒得写了。 姑且写一个简单的例子来演示下CacheResolver是怎么发挥作用的吧。 CacheResolver的实现类如下: 在代码中可以看到,是通过resolveCaches()方法决定了提供哪些缓存。 使用CacheResolver后就有机会可以考虑不注入CacheManager的实例到容器中了,因为CacheResolver会管理会用到的CacheManager的实例。 不过在应用缓存注解的情况下,要记得指定使用哪个CacheResolver,像这样: 就这样了。示例代码都放在了这里:zhyea / multi-cache 参考文档 Using Multiple Cache Managers in Spring

    [阅读更多...]
  • springboot入门12 – SpringBoot MyBatis读写分离

    概述 随业务量增长,数据库读写分离是迟早要面临的问题。另外,公司在上规模后一般也会要求统一采用主从分布式数据库。 我习惯的处理方案是在应用层进行隔离:即将以写为主的业务放在一个应用上,以读为主的业务放在其他应用上。这应该算是最简单粗暴的解决方案了,却也能帮我应对90%需要读写分离的场景。不过总还有10%的特殊场景需要思考下怎样在应用内实现读写分离。 在应用内做读写分离大体上需要考虑三件事情: 多数据源实现 读写请求识别 读写请求分流 其中后两点是执行读写分离的关键。 接下来详细介绍下怎么在Springboot+MyBatis的应用中实现读写分离。这里会用到H2数据库和dbcp2数据库连接池。在测试中不会真的创建一个数据库集群,我们只需要能够验证写入和读取是访问的两个不同的数据库即可。 1. 多数据源实现 之前在《SpringBoot自定义数据源及多数据源配置》这篇文里我有介绍过怎样做多数据源实现。这次的做法也差不多。 下面是在配置文件中做的多数据源配置: 这里配置了两个H2的内存数据库,我们权且当它们是一个集群吧。 然后是在一个配置类DsConfig中读取配置: 和之前那篇文章《SpringBoot自定义数据源及多数据源配置》略有不同,这次是用DataSourceProperties来表示读取的配置信息。 注意不要忽略了@Primary注解,不然会报错。 可以这样使用DataSourceProperties创建DataSource的实例: 这里只是想试试这种方案。按照老路子创建DataSource实例也是OK的,并且还会更简洁: 至此多数据源配置已经完成。 2. 读写请求识别 也看过其他人的读写分离方案,其中读写请求识别这一层多是通过自定义注解+AOP来实现的。这种方案当然没问题,但是稍嫌有些繁琐,如果忘掉了添加注解就会导致意外。我更想要的是一种‘润物细无声’的实现。 之前做过一个为写入数据库的实例赋默认值的方案:《MyBatis写入时null问题统一处理方案》。这个方案的思路是通过自定义实现MyBatis拦截器来拦截写数据库请求并补上未赋值的数据。稍稍变通下,就可以改为拦截并识别查询语句: 查询请求主要是由Executor类的query方法实现的,所以只要针对其进行拦截并执行判断即可。 因为判断读写请求和执行读写分流是在两个环节执行,我们需要找个地方将判断结果存储起来,并且保证线程安全,很自然可以想到使用ThreadLocal执行存储。DsContextHolder就是基于ThreadLocal执行的存储。看下实现: 接下来就可以使用DsContextHolder中存储的信息来执行分流了。 3. 读写请求分流 读写请求分流这一层主要依赖了AbstractRoutingDataSource这个类。核心是下面这个方法: 看名字也能知道,determineTargetDataSource决定了为之后的请求提供哪个数据源。根据代码可以看出来,我们至少需要做两件事: 为resolvedDataSources赋值,即设置相关数据源 实现分流方法determineCurrentLookupKey() 具体如何继承AbstractRoutingDataSource类并实现路由方案可以参考如下代码: 在afterPropertiesSet()方法中完成了自定义数据源的创建和设置,并且还将dsWrite设置为了默认数据源。 方法determineCurrentLookupKey()基于DsContextHolder中存储的内容提供了分流的关键字。 大体上就是这样了。具体实现代码已经上传到了GitHub : zhyea/database-wr End!! 参考文档 mybatis读写分离

    [阅读更多...]