标签:# Spring

Spring AI

前言 当一开始听说Spring-AI项目时是很懵的,什么?Spring开始训练模型了?不应该啊,Java还能卷模型赛道了吗?... 打开官网了解了下,才知道原来是缝合怪: Spring AI is an application framework for AI engineering. Its goal is to apply to the AI domain Spring ecosystem design principles such as portability and modular design and promote using POJOs as the building blocks of an application to the AI domain. 官网地址:https://spring.io/projects/spring-ai 换言之就是虽然Java不能卷算法,但是我可以提供一套封装来让你们调用模型提供的API服务,众所周知,我的抽象封装能力还是很强的。你看你们现在这么多厂商提供服务,不得需要一个统一的门面来减少接入成本嘛... 官网列举了目前国外主流的平台: Chat Models OpenAI Azure Open AI Amazon Bedrock Cohere's Command AI21 Labs' Jurassic-2 Meta's LLama 2 Amazon's Titan Google Vertex AI Palm Google Gemini HuggingFace - access thousands of models, including those from Meta such as Llama2 Ollama - run AI models on your local machine MistralAI Text-to-image Models OpenAI with DALL-E StabilityAI Transcription (audio to text) Models OpenAI ... 数不胜数,更何况还有很多国内大厂开源的模型。 所以Spring提供了一套统一的封装门面,其他的厂商也可以基于门面来实现自己的Client,比如Alibaba就接入了com.alibaba.cloud.ai.tongyi.chat.TongYiChatClient。 这就引申出了本博客,记录了下我接入Spring-Ai(OpenAI)、Spring-Ai-Alibaba(TongYi)的过程。 接入Spring-Ai Spring-Ai的接入相对简单,因为最新的start.spring.io已经维护了OpenAI的依赖包,在IDEA使用Spring-Initializr即可完成初始化。 完成Gradle的初始化之后,得到了一个标准的Spring项目,只需要做一个简单的配置: spring.application.name=spring-ai-demo # 生成结果多样性参数,值在0~2之间,值越大越随机越小越固定,但就算为0也会有随机性 spring.ai.openai.chat.temperature=0.7 spring.ai.chat.client.enabled=true # 如果你需要代理的话 spring.ai.openai.base-url=https://api.xty.app # 填写自己的key spring.ai.openai.api-key=${OPENAI_API_KEY} # 填写你需要使用的模型(也可以使用时代码指定) spring.ai.openai.chat.options.model=gpt-3.5-turbo 接下来只需要编写一个Java的控制器,来接收HTTP请求,就可以完成对OpenAI的对话。 /** * @author imyzt * @date 2024/06/19 * @description AI 入口 */ @RestController @RequestMapping public class AiController { @Resource private ChatClient chatClient; @GetMapping("/ai/chat") Map<String, Object> chat(@RequestParam String question) { ChatClient.ChatClientRequest.CallPromptResponseSpec call = chatClient.prompt(new Prompt(question)).call(); return Map.of("question", question, "answer", call.chatResponse()); } } 整体接入还是比较简单的,但是这里踩了一个坑,不知道是我引入的版本比较新还是什么缘故,它的ChatClient Bean 竟然没有自动注册!所以我还手动注册了一个Bean,代码如下: @Bean public ChatClient chatClient(@Autowired OpenAiChatModel openAiChatModel) { return ChatClient.builder(openAiChatModel).build(); } 代码 https://github.com/imyzt/learning-technology-code/tree/master/dive-in-springboot-projects/spring-ai-demo/spring-ai-demo Spring-Ai-Alibaba OpenAI由于API-KEY的费用蛮高,虽然完成了代码的接入,但是最终我还是没有购买它的API...所以又看了国内的厂商,目前主要是Alibaba完成了Spring-Ai的接入,整体接入其实也很简单,建议首先看一遍官网的例子,接下来可以跟着步骤走一下试试。 创建项目,引入依赖 首先完成一个普通SpringBootWeb项目创建,然后引入Alibaba-Ai的依赖 <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-ai</artifactId> <exclusions> <exclusion> <artifactId>spring-ai-core</artifactId> <groupId>org.springframework.ai</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.springboot.ai</groupId> <artifactId>spring-ai-core</artifactId> <version>1.0.3</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2023.0.1.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> 上面的内容,我把无关紧要的依赖去除了,只保留了核心部分,其中有一个关键点是exclusions了spring-ai-core,因为Alibaba引入的版本太老了。改为自己重新引入最新的版本,其他的和官方文档无差别。 配置 spring: application: name: spring-ai-alibaba-demo cloud: ai: tongyi: api-key: ${TONGYI_KEY} images: enabled: true chat: enabled: true 编写控制器 @Slf4j @RestController @RequestMapping public class AiController { @Resource private ChatClient chatClient; @Resource private ImageClient imageClient; @GetMapping("/ai/chat") public String chat(@RequestParam String question) { ChatResponse call = chatClient.call(new Prompt(question)); return call.getResult().getOutput().getContent(); } @GetMapping("/ai/aigc") public String aigc(@RequestParam String question) { ImageResponse call = imageClient.call(new ImagePrompt(question)); return call.getResult().getOutput().getUrl(); } } 完成上面的步骤,基本就完成了接入,在postman上面输入地址,就可以进行测试了。因为通义不仅有chat,还可以文生图,所以我完成下演示: 文生文 (质量不予置评) 文生图 Spring-Ai-Alibaba还提供了一些示例,在他们的官方Github上,可以参考。 官方还提供了一个简单的HTML来进行页面展示,也可以自己跑一下看看,最终效果如下: 代码 https://github.com/imyzt/learning-technology-code/tree/master/dive-in-springboot-projects/spring-ai-alibaba-demo/spring-ai-alibaba-demo 参考博客 阿里也出手了!Spring CloudAlibaba AI问世了 AI框架之Spring AI与Spring Cloud Alibaba AI使用讲解
Read More ~

Spring 异步初始化

在Spring6.2版本后,支持了异步初始化。 什么是异步初始化?见名知意,就是将Spring项目的初始化过程中的Bean通过异步加载的方式提高启动速度。 在业务系统中通常启动不会特别耗时,但也可以将系统启动过程中需要耗时初始化的Bean通过异步将其并行初始化,节省部分启动时间。本文作为学习Why哥文章之后的简单使用记录。 模拟初始化Bean耗时 ABean @Slf4j public class TestABean { @SneakyThrows public TestABean() { log.info("A Bean开始初始化"); TimeUnit.SECONDS.sleep(5); log.info("A Bean初始化完成"); } } BBean @Slf4j public class TestBBean { @SneakyThrows public TestBBean() { log.info("B Bean开始初始化"); TimeUnit.SECONDS.sleep(6); log.info("B Bean初始化完成"); } } 配置类 @Configuration public class RegisterConfiguration { @Bean public TestABean testABean() { return new TestABean(); } @Bean public TestBBean testBBean() { return new TestBBean(); } } 启动类 @Slf4j @ComponentScan("top.imyzt.learning.spring.startup") public class Application { public static void main(String[] args) { StopWatch stopWatch = new StopWatch("Spring启动"); stopWatch.start(); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Application.class); stopWatch.stop(); System.out.println(stopWatch.prettyPrint()); } } 启动日志 21:39:33.658 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'testABean' 21:39:33.663 [main] INFO top.imyzt.learning.spring.startup.core.TestABean - A Bean开始初始化 21:39:38.667 [main] INFO top.imyzt.learning.spring.startup.core.TestABean - A Bean初始化完成 21:39:38.669 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'testBBean' 21:39:38.670 [main] INFO top.imyzt.learning.spring.startup.core.TestBBean - B Bean开始初始化 21:39:44.673 [main] INFO top.imyzt.learning.spring.startup.core.TestBBean - B Bean初始化完成 StopWatch 'Spring启动': 11.555947229 seconds ------------------------------------------ Seconds % Task name ------------------------------------------ 11.55594723 100% 小结 可以看到,在常规情况下,A和B Bean是串行初始化的,整个初始化耗时11.5s。 异步初始化 在Spring6.2版本中,@Bean注解引入了一个新的属性:bootstrap,默认Bean.Bootstrap.DEFAULT时为串行初始化,当指定为Bean.Bootstrap.BACKGROUND时,Spring会尝试异步初始化该Bean,但是需要配置一个名为bootstrapExecutor的线程池,用作异步初始化时所需的线程。 只需要将配置类稍作修改,就可以将指定的Bean进行异步初始化: @Configuration public class RegisterConfiguration { @Bean(bootstrap = Bean.Bootstrap.BACKGROUND) public TestABean testABean() { return new TestABean(); } @Bean(bootstrap = Bean.Bootstrap.BACKGROUND) public TestBBean testBBean() { return new TestBBean(); } @Bean() public Executor bootstrapExecutor() { return new ThreadPoolExecutor(2, 10, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(1024)); } } 然后我们再查看启动效果: 21:53:49.023 [pool-1-thread-1] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'testABean' 21:53:49.024 [pool-1-thread-1] INFO top.imyzt.learning.spring.startup.core.TestABean - A Bean开始初始化 21:53:49.025 [pool-1-thread-2] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'testBBean' 21:53:49.026 [pool-1-thread-2] INFO top.imyzt.learning.spring.startup.core.TestBBean - B Bean开始初始化 21:53:54.029 [pool-1-thread-1] INFO top.imyzt.learning.spring.startup.core.TestABean - A Bean初始化完成 21:53:55.031 [pool-1-thread-2] INFO top.imyzt.learning.spring.startup.core.TestBBean - B Bean初始化完成 StopWatch 'Spring启动': 6.740055683 seconds ----------------------------------------- Seconds % Task name ----------------------------------------- 6.740055683 100% 可以看到启动时间从11.5s降为6.7s,效果十分显著,在部分需要依赖外部或已知需要耗时初始化的Bean,可以通过此方法进行优化。“我可以不用,你不能没有”。 参考 13年过去了,Spring官方竟然真的支持Bean的异步初始化了! 博客对应代码
Read More ~

CircuitBreak导致ThreadLocal参数丢失问题

背景 使用OpenFeign时,通常会实现RequestInterceptor接口来自定义FeignConfiguration,OpenFeign暴露了feign.RequestTemplate信息,给到我们在发送请求前自定义参数信息的扩展点。 在分布式系统中,通常会将本服务的信息(UserInfo、RequestId)透传至下游服务,从而实现分布式链路追踪等功能,对于像用户信息等,在Web系统中通常使用 ThreadLocal 来存储信息,在自定义的FeignConfiguration中获取ThreadLocal再塞入到feign.RequestTemplate中,实现向下游服务的传递,示例: public class FeignConfiguration implements RequestInterceptor { @Override public void apply(RequestTemplate template) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); String userId = SubjectContext.get().getUserId(); if (null != attributes) { HttpServletRequest request = attributes.getRequest(); template.header("token", request.getHeader("TOKEN")); template.header("userId", userId); } } } 简单的Context示例: public class SubjectContext { protected static ThreadLocal<UserInfo> subjectContext = new ThreadLocal(); public static void remove() { subjectContext.remove(); } public static void set(UserInfo uerInfo) { subjectContext.set(uerInfo); } public static UserInfo get() { return (UserInfo)subjectContext.get(); } } 出现错误 上述代码在常规情况下,是能够按照预期执行的。 但是最近项目引入了CircuitBreaker作为服务熔断的断路器之后,上述代码在执行到SubjectContext.get()时,会抛出空指针,拿不到用户信息。 通过分析CircuitBreaker的源码,最终定位到代码出现在Resilience4JCircuitBreaker内部,在Resilience4JCircuitBreaker中有一个public <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback)方法,方法入参的toRun就是封装过的我们定义的Feign接口,其包装过程在FeignCircuitBreakerInvocationHandler#asSupplier代码中,如下: private Supplier<Object> asSupplier(final Method method, final Object[] args) { final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); return () -> { try { RequestContextHolder.setRequestAttributes(requestAttributes); // 执行我们的真正方法 return dispatch.get(method).invoke(args); } catch (RuntimeException throwable) { throw throwable; } catch (Throwable throwable) { throw new RuntimeException(throwable); } }; } Spring Cloud CircuitBreaker Resilience4j 提供了两种实现: 使用 Semaphores 的 SemaphoreBulkhead。 一个 FixedThreadPoolBulkhead,它使用一个有界队列和一个固定的线程池。 默认情况下,Spring Cloud CircuitBreaker Resilience4j 使用 FixedThreadPoolBulkhead。要修改默认行为以使用 SemaphoreBulkhead,请将属性 spring.cloud.circuitbreaker.resilience4j.enableSemaphoreDefaultBulkhead 设为 true。 正是由于上述原因,默认将我们的FeignConfiguration提交给了线程池,由于我们使用的是ThreadLocal导致线程本地变量没有向子线程传递,在执行FeignConfiguration时子线程无法拿到Context信息,最终导致程序的报错。 解决办法 通过分析源码我们发现,执行任务的线程池Resilience4JCircuitBreaker#executorService是由外部传递过来进行初始化的,调用方在Resilience4JCircuitBreakerFactory#create(java.lang.String, java.lang.String, java.util.concurrent.ExecutorService) 在Resilience4JCircuitBreakerFactory中发现,是由本实例在create方法被调用时传入的本类的成员变量,即: private ExecutorService executorService = Executors.newCachedThreadPool(); private ConcurrentHashMap<String, ExecutorService> executorServices = new ConcurrentHashMap<>(); 而我们在没有定义自定义Feign Group时,默认使用的就是executorService,在本类中有一个Resilience4JCircuitBreakerFactory#configureExecutorService方法专门保留了外部传入自定义线程池的扩展,我们可以自己实现创建一个支持传递Context到子线程的线程池,即可将参数向下传递,比如像这样: @Configurable @AllArgsConstructor public class CircuitBreakerConfiguration implements ApplicationRunner { private final Resilience4JCircuitBreakerFactory factory; @Override public void run(ApplicationArguments args) throws Exception { ContextThreadPoolExecutor contextThreadPoolExecutor = new ContextThreadPoolExecutor(2, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(1024)); // **change ThreadPoolExecutor** factory.configureExecutorService(contextThreadPoolExecutor); } public static class ContextThreadPoolExecutor extends ThreadPoolExecutor { public ContextThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); } public void execute(Runnable command) { super.execute(wrap(command)); } private static Runnable wrap(Runnable runnable) { **SubjectContext context = SubjectContext.getContext();** return () -> { // 将参数向下传递 **SubjectContext.setContext(context);** try { runnable.run(); } finally { **SubjectContext.clear();** } }; } } } 后记 上述的方案只解决了没有自定义Group的情况,官方在自定义Group的情况下是没有保留扩展位的,所以给官方提了一个MR并且已成功合并到主分支,如下: Customizable groupExecutorService #180
Read More ~

Spring中使用到的设计模式

设计模式的基本原则: 开闭原则 单一职责原则 里氏替换原则 依赖倒置原则 Spring中用到的模式: HandlerAdapter,适配器模式 BeanFactory,简单工厂模式 FactoryBean,工厂方法模式 Controller的单例模式,也可以是原型模式 Service@Transactional,代理模式 @EventListener,观察者模式 JdbcTemplate,RestTemplate,模板方法模式 TransactionAwareCacheDecorator,装饰器模式 AopProxy,策略模式 其他在业务中常用的模式: 业务中涉及的模式: 策略模式: 不同数据的不同处理结果 观察者模式: 事件监听和分发(不同处理类型) 门面模式: 不同活动的不同参数,统一门面,采用Jackson的类型映射子类 状态模式: 支付的不同状态,通过事件和状态流转
Read More ~

通过Spring工具类,父类获取子类泛型(多态泛型传参)

在平时需要写抽象策略处理器时,希望传给处理器执行器的对象是泛型,避免每个子类都写强制类型转换的代码,通常需要获取子类的泛型,然后才能够避免写@SuppressWarnings,可以通过如下工具类在Abstract类上面获取到子类泛型。 ResolvableType[] generics = ResolvableType.forClass(this.getClass()).getSuperType().getGenerics(); // generics[0]是第一个泛型 T o = (T) generalResult.toJavaObject(generics[0].resolve());
Read More ~