• [技术干货] 【监控】spring actuator源码速读-转载
     1.前言 版本:spring-boot-starter-actuator  2.6.3  阅读源码一定要带着疑问去阅读,这个疑问就是你阅读的主线,不然在浩如烟海的源码里面很容易迷路。我们当前的疑问是什么?之前我们已经聊过spring actuator的使用了:  Spring Boot 监控_springboot 监控-CSDN博客  本文要搞清楚的两个问题在于:  EndPoint是怎么被注入IOC又怎么暴露出去能通过HTTP访问到的?  EndPoint是怎么实现监控能力的?  2.先搂一眼EndPoint 首先我们找一个EndPoint来看看,此处以HealthEndPoint为例。点看源码我们可以看到这个EndPoint被@EndPoint注解所注释,id为health。然后其中的2个方法被@ReadOperation所注释:   这里其实猜都能猜到被@EndPoint注解,然后被注解的类被归类为EndPoint,然后被集中暴露出去,变成可访问的。  3.EndPoint如何被注入 我们是通过stater来引入actuator的,Spring Boot体系内如何注入stater的?那肯定是通过autoConfiguration来的撒。点进actuator的配置文件也可以看到:   于是我们来到spring-boot-starter-actuator来看看,看看它的spring.factories里面注入了些什么:  见名知意了,这些众多的XXXautoConfiguration是拿来做什么的就不必多说了吧,health、metrics......分门别类,各种EndPoint的autoConfiguration。   我们来看看HealthEndpointAutoConfiguration里面做了什么:  其实就是加载了HealthEndpointConfiguration、ReactiveHealthEndpointConfiguration、HealthEndpointWebExtensionConfiguration、HealthEndpointReactiveWebExtensionConfiguration这几个类  @Configuration(     proxyBeanMethods = false ) @ConditionalOnAvailableEndpoint(     endpoint = HealthEndpoint.class ) @EnableConfigurationProperties({HealthEndpointProperties.class}) @Import({HealthEndpointConfiguration.class, ReactiveHealthEndpointConfiguration.class, HealthEndpointWebExtensionConfiguration.class, HealthEndpointReactiveWebExtensionConfiguration.class}) public class HealthEndpointAutoConfiguration {     public HealthEndpointAutoConfiguration() {     } } 我们先看HealthEndpointConfiguration,它里面向IOC中注入了health的Registry,HealthContributorRegistry中注册了HealthContributor类型的实体。   我们随便打开一个health的EndPoint,发现它都继承同一个父类:   而这个父类实现了HealthContributor接口:   所以其实就是在将注册器注入IOC的时候,就将所有属于该类型的EndPoint注册到注册器中了。  4.EndPoint如何被暴露 4.1.如何通过http暴露 在SpringBoot体系中,谁来负责http请求?那当然是SpringMVC的DispatcherServlet。把path和对应处理的类注册到DispatcherServlet中就行了。spring actuator就是这样对外通过HTTP的方式暴露EndPoint的。  回到spring.factories,找ManagementContextAutoConfiguration,这个类中完成了通过HTTP的方式来暴露EndPoint的过程:   这个类的代码并不多,我们去掉不要的部分,把和对外暴露EndPoint相关的代码保留,来读一下:  @ManagementContextConfiguration(     proxyBeanMethods = false ) @ConditionalOnWebApplication(     type = Type.SERVLET )//只能在Web环境中生效 public class ServletEndpointManagementContextConfiguration {     public ServletEndpointManagementContextConfiguration() {     } ​     @Configuration(         proxyBeanMethods = false     )     @ConditionalOnClass({DispatcherServlet.class})//当SpringMVC存在时向IOC中注入     public static class WebMvcServletEndpointManagementContextConfiguration {         public WebMvcServletEndpointManagementContextConfiguration() {         }         @Bean         public ServletEndpointRegistrar servletEndpointRegistrar(WebEndpointProperties properties, ServletEndpointsSupplier servletEndpointsSupplier, DispatcherServletPath dispatcherServletPath) {             return new ServletEndpointRegistrar(dispatcherServletPath.getRelativePath(properties.getBasePath()), servletEndpointsSupplier.getEndpoints());//核心的一步,将EndPoint和对于的Path注册给DispatcherServlet         }     } }  最后就是开头我们看见的在HealthEndPoint被@ReadOperation注解的方法,就相当于@RequetMapping,拿来处理读请求的。  4.2.如何通过jmx暴露 jmx的对外暴露更简单。直接找JmxEndpointAutoConfiguration:   进去整个逻辑一目了然,去扫Jmx的EndPoint然后注册进mBeanServer里:   5.EndPoint是怎么实现监控能力的 spring actuator获取各种监控的值是怎么获取到的?  内置指标获取: Spring Boot 提供了一些内置的监控指标获取器,用于获取常见的监控数据,比如 JVM 内存使用情况、系统负载、数据库连接池状态等。这些指标获取器会周期性地获取数据,并将其暴露为 Actuator 端点,以便外部系统或者工具可以通过相应的接口来获取。例如,MemoryHealthIndicator 获取 JVM 内存使用情况,DataSourceHealthIndicator 获取数据库连接池状态等。  自定义指标获取: 除了内置的指标获取器外,开发者还可以通过实现 HealthIndicator 接口来自定义监控指标获取器,用于获取应用程序特定的监控数据。HealthIndicator 接口定义了一个 health() 方法,用于返回健康状态信息。开发者可以在 health() 方法中编写自定义的监控逻辑,比如检查某个依赖服务的可用性、计算某个指标的值等。  JMX 获取: Spring Actuator 还可以通过 Java Management Extensions (JMX) API 来获取一些系统级的监控数据,比如 JVM 运行时信息、操作系统信息等。Spring Actuator 中的一些监控指标获取器会使用 JMX API 来获取数据,然后将其暴露为 Actuator 端点。例如,JvmInfoContributor 使用 JMX API 来获取 JVM 运行时信息。  系统调用获取: 有些监控数据可能需要通过系统调用来获取,比如获取操作系统的 CPU 使用率、磁盘使用情况等。Spring Actuator 中的一些监控指标获取器会使用系统调用来获取这些数据,然后将其暴露为 Actuator 端点。  6.知道这些的意义是什么 本文是作者java监控系列文章的第三篇,之前两篇文章我们着重讲了java监控的基石——JMX。  【JMX】JAVA监控的基石-CSDN博客  详解tomcat中的jmx监控-CSDN博客  在spring actuator里面我们知道了目前市面上一个成熟的框架是如何通过http、JMX等不同方式来对外暴露监控能力的。基本上走到这里我们就已经对JAVA整个的监控技术体系最核心的部分有了认识了。作为监控框架来说核心点有哪些?无非是:  获取数据  对外暴露口子  监控的核心肯定是怎么获取数据以及如何将获取的数据暴露出去,只要这两点搞定了,后面的对接各种可视化平台就很好办了。所有知道为啥这篇文章为啥要关心spring actuator这些地方了吧,主要是看看实现思想。 ————————————————                              版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                          原文链接:https://blog.csdn.net/Joker_ZJN/article/details/136154345 
  • [专题汇总] 快进来宝子们,二月份干货汇总来了。
     大家好,二月的合集又来了,本次涵盖了java,linux,spirng诸多内容供大家学习。 1.Vue中的$nextTick有什么作用?【转】 https://bbs.huaweicloud.com/forum/thread-02109144317537776031-1-1.html  2.基于Python的地图绘制教程【转】 https://bbs.huaweicloud.com/forum/thread-0240144319944756030-1-1.html  3.hcache 介绍(1)--Ehcache 功能特性 【转】 https://bbs.huaweicloud.com/forum/thread-0268144320319800023-1-1.html  4.Java 操作 XML(1)--DOM 方式处理 XML【转】 https://bbs.huaweicloud.com/forum/thread-02109144320463478034-1-1.html  5.Java操作XML(3)--StAX方式处理XML 【转】 https://bbs.huaweicloud.com/forum/thread-0240144320615655031-1-1.html  6.Java操作XML(4)--使用woodstox处理XML【转】 https://bbs.huaweicloud.com/forum/thread-0270144320690142025-1-1.html  7.Java操作XML(5)--使用JDOM处理XML【转】 https://bbs.huaweicloud.com/forum/thread-0240144320817429032-1-1.html  8.Java操作XML(6)--使用dom4j处理XML【转】 https://bbs.huaweicloud.com/forum/thread-02101144320893290020-1-1.html  9.基于原生Go语言开发一个博客系统【转】 https://bbs.huaweicloud.com/forum/thread-0269144558086016006-1-1.html  10.详解Golang如何使用Debug库优化代码【转】 https://bbs.huaweicloud.com/forum/thread-0225144558556243005-1-1.html  11.Python3中的指针你了解吗【转】 https://bbs.huaweicloud.com/forum/thread-0274144558593208011-1-1.html  12.Python绘图实现坐标轴共享与复用详解【转】 https://bbs.huaweicloud.com/forum/thread-02104144558721772010-1-1.html  13.使用Golang开发一个简易版shell【转】 https://bbs.huaweicloud.com/forum/thread-0226144559187797004-1-1.html  14.python导入其它py文件的实现步骤【转】 https://bbs.huaweicloud.com/forum/thread-0284144559442791006-1-1.html  15.Python property函数的具体使用【转】 https://bbs.huaweicloud.com/forum/thread-0259144559705810008-1-1.html  16.正则表达式的神奇世界之表达、匹配和提取全解析【转】 https://bbs.huaweicloud.com/forum/thread-02104144569438876012-1-1.html  17.正则去除中括号(符号)及里面包含的内容(最新推荐)【转】 https://bbs.huaweicloud.com/forum/thread-0226144569604407008-1-1.html  18.Javaweb项目启动Tomcat常见的报错解决方案【转】 https://bbs.huaweicloud.com/forum/thread-0274144571948983013-1-1.html  19.SpringBoot+Vue前后端分离实现审核功能的示例【转】 https://bbs.huaweicloud.com/forum/thread-0269144572025869007-1-1.html  20.Java中Collections.sort()排序方法举例详解【转】 https://bbs.huaweicloud.com/forum/thread-0269144572170830008-1-1.html  21.Java中回调函数 (callback) 及其实际应用场景【转】 https://bbs.huaweicloud.com/forum/thread-0259144572260172012-1-1.html  22.Redis实现商品秒杀的示例代码【转】 https://bbs.huaweicloud.com/forum/thread-0274144574350854015-1-1.html 
  • [技术干货] CORS就是跨域吗【转】
    首先,跨域的域是什么?跨域的英文是:Cross-Origin。Origin 中文含义为:起源,源头,出生地。在跨域中,"域"指的是一个 Web 资源(比如网页、脚本、图片等)的源头。包括该资源的协议、主机名、端口号。在同源策略中,如果两个资源的域相同,则它们属于同一域,可以自由进行交互和共享数据。反之,如果两个资源的域不同,就会出现跨域问题。这时就需要特殊的方式来处理,如跨域资源共享(CORS)。那什么是同源策略?同源策略(Same-Origin Policy)是浏览器中的一项安全机制,用于保护用户的隐私和安全。它限制了一个网页或者脚本只能从同一个源加载的资源进行访问,而不能访问其他来源的资源。这样做可以防止恶意网站利用用户身份信息进行跨站请求伪造(CSRF)攻击,保护用户的数据安全。什么是跨站请求伪造?跨站请求伪造(CSRF,Cross-Site Request Forgery)是一种网络攻击方式。在 CSRF 攻击中,攻击者利用已认证的用户身份(例如用户在银行网站上登录后的会话信息)来伪造请求,以执行未经授权的操作。举个例子:我登录了银行网站,浏览器根据我的登录信息生成了一个会话令牌,也就是 session token。但是这个令牌被而恶意网站给拿到了,它拿着我的 token 去服务器发送请求。就可以把我银行卡里的 29 块八毛五全部转走。但是如果有同源策略的限制,恶意网站就无法直接发送请求到银行。我的 29 块八毛五就可以保住。因为恶意网站的域名与银行网站的域名不同,浏览器会阻止这种抢劫行为。什么是跨域资源共享(CORS)?为了防止被面试官笑话,这里一定要知道:跨域资源共享(CORS,Cross-Origin Resource Sharing)是一种用来解决由于浏览器的同源策略而导致的跨域请求问题的一种机制。浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。什么是简单请求?只要同时满足以下两大条件,就属于简单请求。(1)请求方法是以下三种方法之一: - HEAD - GET - POST (2)HTTP的头信息不超出以下几种字段: - Accept - Accept-Language - Content-Language - Last-Event-ID - Content-Type:只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain简单请求的工作流程如下:1. 浏览器在请求中增加一个 Origin 头部字段,其中包含当前页面的源信息(协议、主机、端口)。2. 服务器在收到这个请求后,会根据请求中的 Origin 头部信息来判断是否允许该请求。3. 如果服务器允许该请求,会在响应头部中包含一个 Access-Control-Allow-Origin 头部,"*"表示允许所有来源。4. 浏览器在收到响应后,决定是否允许页面访问该资源。什么是非简单请求?不是简单请求的,就是非简单请求。非简单请求它非简单在哪?或者说:它非简单又能怎么样?非简单请求在发起正式请求之前,会先发起一个预检请求。什么是预检请求?预检请求是用于在实际的跨域请求之前进行探测和验证,以确保服务器能够正确处理,预防跨域请求可能会引发的安全性问题。一句话就是:我去前面探探路!只有得到服务器的肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。实际 java 开发中的 CORS 解决跨域配置长这样:@Configuration public class CorsConfig implements WebMvcConfigurer {     @Override     public void addCorsMappings(CorsRegistry registry) {         // 允许所有的URL路径都可以跨域访问         registry.addMapping("/**")             // 允许所有来源(即允许任何域名)的请求跨域访问             .allowedOrigins("*")             // 允许发送身份验证信息(如cookies、HTTP身份验证或客户端SSL证明)             .allowCredentials(true)             // 允许跨域请求的HTTP方法,包括GET、POST、PUT、DELETE和OPTIONS。             .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")             // 预检请求(OPTIONS请求)的有效期,单位为秒             .maxAge(3600);     } }还有别的方式可以解决跨域问题吗?有的。使用 Nginx 部署为同一域。什么意思呢?就是说 Nginx 作为唯一域,代理所有服务端,在客户端眼里,只有 Nginx 这一个域,也就不存在跨域问题,由 Nginx 拿到请求再分发给对应服务器。这里我们就不再展开。转载自https://www.cnblogs.com/cosimo/p/18023596
  • [技术干货] Spring Cloud与Spring的区别
    前言随着微服务架构的兴起,Spring Cloud逐渐崭露头角,成为了构建分布式系统的重要工具。对于很多初次接触Spring Cloud的开发者来说,可能不太清楚它与传统的Spring框架有何区别。本文旨在探讨Spring Cloud与Spring之间的主要差异。一、Spring框架简介Spring是一个开源的Java平台,它提供了一套全面的编程和配置模型,用于构建企业级应用程序。Spring的核心特性包括依赖注入(DI)、面向切面编程(AOP)、数据访问抽象等。通过Spring,开发者可以更加高效、简洁地开发应用程序。二、Spring Cloud简介Spring Cloud是基于Spring Boot的一套微服务工具集,它提供了一整套微服务解决方案,包括服务发现、配置管理、熔断器、负载均衡等。Spring Cloud的目标是让微服务架构的搭建变得更加简单、快速和可靠。三、Spring Cloud与Spring的主要区别关注点不同:Spring框架主要关注的是应用程序的本身,提供了一系列的基础功能,如依赖注入、事务管理等。而Spring Cloud则更加关注微服务架构中的各个组件和服务之间的通信与协作,为构建分布式系统提供了丰富的工具和解决方案。组件和服务的集成:Spring Cloud集成了许多优秀的开源项目,如Netflix的Eureka、Hystrix、Zuul等,这些组件共同构成了微服务架构的核心部分。而Spring本身并不包含这些组件,需要开发者自行集成。服务治理:Spring Cloud提供了强大的服务治理功能,包括服务发现、配置管理、熔断器、负载均衡等。这些功能使得微服务架构更加健壮、可靠和易于维护。相比之下,Spring本身并不提供这些服务治理功能。部署和扩展性:Spring Cloud的设计初衷就是为了支持快速部署和横向扩展,它允许开发者将应用程序拆分成多个独立的服务,每个服务都可以独立部署和扩展。而传统的Spring应用程序通常需要整体部署,扩展性相对较差。与Spring Boot的整合:Spring Cloud是建立在Spring Boot基础之上的,因此它充分利用了Spring Boot的优点,如自动配置、快速启动等。这使得开发者在构建微服务应用程序时,可以更加高效、简洁地开发、部署和维护。四、总结综上所述,Spring Cloud与Spring的主要区别在于它们的关注点、组件和服务的集成、服务治理、部署和扩展性以及与Spring Boot的整合。Spring Cloud作为一套微服务工具集,为构建分布式系统提供了丰富的解决方案,使得开发者可以更加轻松地应对复杂的业务需求。然而,在使用Spring Cloud时,也需要关注其复杂性、学习曲线和潜在的兼容性问题。因此,在选择使用Spring Cloud还是传统的Spring框架时,需要根据项目的具体需求和团队的实际情况进行权衡。
  • [技术干货] 多线程事务回滚的处理【转】
     转自 : https://blog.csdn.net/weixin_43225491/article/details/117705686背景介绍  1.最近有一个大数据量插入的操作入库的业务场景,需要先做一些其他修改操作,然后在执行插入操作,由于插入数据可能会很多,用到多线程去拆分数据并行处理来提高响应时间,如果有一个线程执行失败,则全部回滚.  2.在spring中可以使用@Transactional注解去控制事务,使出现异常时会进行回滚,在多线程中,这个注解则不会生效,如果主线程需要先执行一些修改数据库的操作,当子线程在进行处理出现异常时,主线程修改的数据则不会回滚,导致数据错误。 3.下面用一个简单示例演示多线程事务. 公用的类和方法  /**      * 平均拆分list方法.      * @param source      * @param n      * @param <T>      * @return      */     public static <T> List<List<T>> averageAssign(List<T> source,int n){         List<List<T>> result=new ArrayList<List<T>>();         int remaider=source.size()%n;          int number=source.size()/n;          int offset=0;//偏移量         for(int i=0;i<n;i++){             List<T> value=null;             if(remaider>0){                 value=source.subList(i*number+offset, (i+1)*number+offset+1);                 remaider--;                 offset++;             }else{                 value=source.subList(i*number+offset, (i+1)*number+offset);             }             result.add(value);         }         return result;     }      1     2     3     4     5     6     7     8     9     10     11     12     13     14     15     16     17     18     19     20     21     22     23     24     25  /**  线程池配置  * @version V1.0  * @since 2021-06-08 15:39  */ public class ExecutorConfig {     private static int maxPoolSize = Runtime.getRuntime().availableProcessors();     private volatile static ExecutorService executorService;     public static ExecutorService getThreadPool() {         if (executorService == null){             synchronized (ExecutorConfig.class){                 if (executorService == null){                     executorService =  newThreadPool();                 }             }         }         return executorService;     }      private static  ExecutorService newThreadPool(){         int queueSize = 500;         int corePool = Math.min(5, maxPoolSize);         return new ThreadPoolExecutor(corePool, maxPoolSize, 10000L, TimeUnit.MILLISECONDS,             new LinkedBlockingQueue<>(queueSize),new ThreadPoolExecutor.AbortPolicy());     }     private ExecutorConfig(){} }      1     2     3     4     5     6     7     8     9     10     11     12     13     14     15     16     17     18     19     20     21     22     23     24     25     26  /** 获取sqlSession  * @author 86182  * @version V1.0  * @since 2021-06-03 15:08  */ @Component public class SqlContext {     @Resource     private SqlSessionTemplate sqlSessionTemplate;      public SqlSession getSqlSession(){         SqlSessionFactory sqlSessionFactory = sqlSessionTemplate.getSqlSessionFactory();         return sqlSessionFactory.openSession();     } }      1     2     3     4     5     6     7     8     9     10     11     12     13     14     15  示例事务不成功操作    /**      * 测试多线程事务.      * @param employeeDOList      */     @Override     @Transactional     public void saveThread(List<EmployeeDO> employeeDOList) {         try {             //先做删除操作,如果子线程出现异常,此操作不会回滚             this.getBaseMapper().delete(null);             //获取线程池             ExecutorService service = ExecutorConfig.getThreadPool();             //拆分数据,拆分5份             List<List<EmployeeDO>> lists=averageAssign(employeeDOList,2);             //执行的线程             Thread []threadArray = new Thread[lists.size()];             //监控子线程执行完毕,再执行主线程,要不然会导致主线程关闭,子线程也会随着关闭             CountDownLatch countDownLatch = new CountDownLatch(lists.size());             AtomicBoolean atomicBoolean = new AtomicBoolean(false);             for (int i =0;i<lists.size();i++){                 List<EmployeeDO> list  = lists.get(i);                 threadArray[i] =  new Thread(() -> {                     try {                         if (!atomicBoolean.get()){                             throw new ServiceException("001","出现异常");                         }                         //批量添加,mybatisPlus中自带的batch方法                         this.saveBatch(list);                     }finally {                         countDownLatch.countDown();                     }                 });             }             for (int i = 0; i <lists.size(); i++){                 service.execute(threadArray[i]);             }             //当子线程执行完毕时,主线程再往下执行             countDownLatch.await();             System.out.println("添加完毕");         }catch (Exception e){             log.info("error",e);             throw new ServiceException("002","出现异常");         }     }      1     2     3     4     5     6     7     8     9     10     11     12     13     14     15     16     17     18     19     20     21     22     23     24     25     26     27     28     29     30     31     32     33     34     35     36     37     38     39     40     41     42     43     44     45  数据库中存在一条数据: 在这里插入图片描述  //测试用例 @RunWith(SpringRunner.class) @SpringBootTest(classes = { ThreadTest01.class, MainApplication.class}) public class ThreadTest01 {      @Resource     private EmployeeBO employeeBO;      /**      *   测试多线程事务.      * @throws InterruptedException      */     @Test     public  void MoreThreadTest2() throws InterruptedException {         int size = 10;         List<EmployeeDO> employeeDOList = new ArrayList<>(size);         for (int i = 0; i<size;i++){             EmployeeDO employeeDO = new EmployeeDO();             employeeDO.setEmployeeName("lol"+i);             employeeDO.setAge(18);             employeeDO.setGender(1);             employeeDO.setIdNumber(i+"XX");             employeeDO.setCreatTime(Calendar.getInstance().getTime());             employeeDOList.add(employeeDO);         }         try {             employeeBO.saveThread(employeeDOList);             System.out.println("添加成功");         }catch (Exception e){             e.printStackTrace();         }     } }      1     2     3     4     5     6     7     8     9     10     11     12     13     14     15     16     17     18     19     20     21     22     23     24     25     26     27     28     29     30     31     32     33  测试结果: 在这里插入图片描述 在这里插入图片描述 可以发现子线程组执行时,有一个线程执行失败,其他线程也会抛出异常,但是主线程中执行的删除操作,没有回滚,Transactional注解没有生效. 使用sqlSession控制手动提交事务   @Resource    SqlContext sqlContext;  /**      * 测试多线程事务.      * @param employeeDOList      */     @Override     public void saveThread(List<EmployeeDO> employeeDOList) throws SQLException {         // 获取数据库连接,获取会话(内部自有事务)         SqlSession sqlSession = sqlContext.getSqlSession();         Connection connection = sqlSession.getConnection();         try {             // 设置手动提交             connection.setAutoCommit(false);             //获取mapper             EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);             //先做删除操作             employeeMapper.delete(null);             //获取执行器             ExecutorService service = ExecutorConfig.getThreadPool();             List<Callable<Integer>> callableList  = new ArrayList<>();             //拆分list             List<List<EmployeeDO>> lists=averageAssign(employeeDOList, 5);             AtomicBoolean atomicBoolean = new AtomicBoolean(true);             for (int i =0;i<lists.size();i++){                 if (i==lists.size()-1){                     atomicBoolean.set(false);                 }                 List<EmployeeDO> list  = lists.get(i);                 //使用返回结果的callable去执行,                 Callable<Integer> callable = () -> {                     if (!atomicBoolean.get()){                         throw new ServiceException("001","出现异常");                     }                   return employeeMapper.saveBatch(list);                 };                 callableList.add(callable);             }             //执行子线程            List<Future<Integer>> futures = service.invokeAll(callableList);             for (Future<Integer> future:futures) {             //如果有一个执行不成功,则全部回滚                 if (future.get()<=0){                     connection.rollback();                      return;                 }             }             connection.commit();             System.out.println("添加完毕");         }catch (Exception e){             connection.rollback();             log.info("error",e);             throw new ServiceException("002","出现异常");         }finally {              connection.close();          }     }              1     2     3     4     5     6     7     8     9     10     11     12     13     14     15     16     17     18     19     20     21     22     23     24     25     26     27     28     29     30     31     32     33     34     35     36     37     38     39     40     41     42     43     44     45     46     47     48     49     50     51     52     53     54     55     56     57     58     59    // sql    <insert id="saveBatch" parameterType="List">     INSERT INTO     employee (employee_id,age,employee_name,birth_date,gender,id_number,creat_time,update_time,status)     values         <foreach collection="list" item="item" index="index" separator=",">         (         #{item.employeeId},         #{item.age},         #{item.employeeName},         #{item.birthDate},         #{item.gender},         #{item.idNumber},         #{item.creatTime},         #{item.updateTime},         #{item.status}             )         </foreach>     </insert>      1     2     3     4     5     6     7     8     9     10     11     12     13     14     15     16     17     18     19  数据库中一条数据: 在这里插入图片描述 测试结果:抛出异常, 在这里插入图片描述 删除操作的数据回滚了,数据库中的数据依旧存在,说明事务成功了. 在这里插入图片描述  成功操作示例:   @Resource     SqlContext sqlContext;     /**      * 测试多线程事务.      * @param employeeDOList      */     @Override     public void saveThread(List<EmployeeDO> employeeDOList) throws SQLException {         // 获取数据库连接,获取会话(内部自有事务)         SqlSession sqlSession = sqlContext.getSqlSession();         Connection connection = sqlSession.getConnection();         try {             // 设置手动提交             connection.setAutoCommit(false);             EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);             //先做删除操作             employeeMapper.delete(null);             ExecutorService service = ExecutorConfig.getThreadPool();             List<Callable<Integer>> callableList  = new ArrayList<>();             List<List<EmployeeDO>> lists=averageAssign(employeeDOList, 5);             for (int i =0;i<lists.size();i++){                 List<EmployeeDO> list  = lists.get(i);                 Callable<Integer> callable = () -> employeeMapper.saveBatch(list);                 callableList.add(callable);             }             //执行子线程            List<Future<Integer>> futures = service.invokeAll(callableList);             for (Future<Integer> future:futures) {                 if (future.get()<=0){                     connection.rollback();                      return;                 }             }             connection.commit();             System.out.println("添加完毕");         }catch (Exception e){             connection.rollback();             log.info("error",e);             throw new ServiceException("002","出现异常");            // throw new ServiceException(ExceptionCodeEnum.EMPLOYEE_SAVE_OR_UPDATE_ERROR);         }     }      1     2     3     4     5     6     7     8     9     10     11     12     13     14     15     16     17     18     19     20     21     22     23     24     25     26     27     28     29     30     31     32     33     34     35     36     37     38     39     40     41     42  测试结果: blog.csdnimg.cn/20210608165840483.png)  数据库中数据: 删除的删除了,添加的添加成功了,测试成功. 在这里插入图片描述 ————————————————                              版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。                          原文链接:https://blog.csdn.net/weixin_43225491/article/details/117705686 
  • [技术干货] 40 个 SpringBoot 常用注解:让生产力爆表!【转】
    企业开发项目SpringBoot已经是必备框架了,其中注解是开发中的小工具,用好了开发效率大大提升,当然用错了也会引入缺陷。一、Spring Web MVC与Spring Bean注解Spring Web MVC注解@RequestMapping@RequestMapping注解的主要用途是将Web请求与请求处理类中的方法进行映射。Spring MVC和Spring WebFlux都通过RquestMappingHandlerMapping和RequestMappingHndlerAdapter两个类来提供对@RequestMapping注解的支持。@RequestMapping注解对请求处理类中的请求处理方法进行标注;@RequestMapping注解拥有以下的六个配置属性:value:映射的请求URL或者其别名method:兼容HTTP的方法名params:根据HTTP参数的存在、缺省或值对请求进行过滤header:根据HTTP Header的存在、缺省或值对请求进行过滤consume:设定在HTTP请求正文中允许使用的媒体类型product:在HTTP响应体中允许使用的媒体类型提示:在使用@RequestMapping之前,请求处理类还需要使用@Controller或@RestController进行标记,公众 号Java精选,回复java面试,获取面试资料,支持在线刷题。下面是使用@RequestMapping的两个示例:@RequestMapping还可以对类进行标记,这样类中的处理方法在映射请求路径时,会自动将类上@RequestMapping设置的value拼接到方法中映射路径之前,如下:@RequestBody@RequestBody在处理请求方法的参数列表中使用,它可以将请求主体中的参数绑定到一个对象中,请求主体参数是通过HttpMessageConverter传递的,根据请求主体中的参数名与对象的属性名进行匹配并绑定值。此外,还可以通过@Valid注解对请求主体中的参数进行校验。下面是一个使用@RequestBody的示例:@GetMapping@GetMapping注解用于处理HTTP GET请求,并将请求映射到具体的处理方法中。具体来说,@GetMapping是一个组合注解,它相当于是@RequestMapping(method=RequestMethod.GET)的快捷方式。下面是@GetMapping的一个使用示例:@PostMapping@PostMapping注解用于处理HTTP POST请求,并将请求映射到具体的处理方法中。@PostMapping与@GetMapping一样,也是一个组合注解,它相当于是@RequestMapping(method=HttpMethod.POST)的快捷方式。下面是使用@PostMapping的一个示例:@PutMapping@PutMapping注解用于处理HTTP PUT请求,并将请求映射到具体的处理方法中,@PutMapping是一个组合注解,相当于是@RequestMapping(method=HttpMethod.PUT)的快捷方式。下面是使用@PutMapping的一个示例:@DeleteMapping@DeleteMapping注解用于处理HTTP DELETE请求,并将请求映射到删除方法中。@DeleteMapping是一个组合注解,它相当于是@RequestMapping(method=HttpMethod.DELETE)的快捷方式。下面是使用@DeleteMapping的一个示例:@PatchMapping@PatchMapping注解用于处理HTTP PATCH请求,并将请求映射到对应的处理方法中。@PatchMapping相当于是@RequestMapping(method=HttpMethod.PATCH)的快捷方式。下面是一个简单的示例:@ControllerAdvice@ControllerAdvice是@Component注解的一个延伸注解,Spring会自动扫描并检测被@ControllerAdvice所标注的类。@ControllerAdvice需要和@ExceptionHandler、@InitBinder以及@ModelAttribute注解搭配使用,主要是用来处理控制器所抛出的异常信息。首先,我们需要定义一个被@ControllerAdvice所标注的类,在该类中,定义一个用于处理具体异常的方法,并使用@ExceptionHandler注解进行标记。此外,在有必要的时候,可以使用@InitBinder在类中进行全局的配置,还可以使用@ModelAttribute配置与视图相关的参数。使用@ControllerAdvice注解,就可以快速的创建统一的,自定义的异常处理类。下面是一个使用@ControllerAdvice的示例代码:@ResponseBody@ResponseBody会自动将控制器中方法的返回值写入到HTTP响应中。特别的,@ResponseBody注解只能用在被@Controller注解标记的类中。如果在被@RestController标记的类中,则方法不需要使用@ResponseBody注解进行标注。@RestController相当于是@Controller和@ResponseBody的组合注解。下面是使用该注解的一个示例@ExceptionHandler@ExceptionHander注解用于标注处理特定类型异常类所抛出异常的方法。当控制器中的方法抛出异常时,Spring会自动捕获异常,并将捕获的异常信息传递给被@ExceptionHandler标注的方法。面试宝典:https://www.yoodb.com下面是使用该注解的一个示例:@ResponseStatus@ResponseStatus注解可以标注请求处理方法。使用此注解,可以指定响应所需要的HTTP STATUS。特别地,我们可以使用HttpStauts类对该注解的value属性进行赋值。下面是使用@ResponseStatus注解的一个示例:@PathVariable@PathVariable注解是将方法中的参数绑定到请求URI中的模板变量上。可以通过@RequestMapping注解来指定URI的模板变量,然后使用@PathVariable注解将方法中的参数绑定到模板变量上。特别地,@PathVariable注解允许我们使用value或name属性来给参数取一个别名。下面是使用此注解的一个示例:模板变量名需要使用{ }进行包裹,如果方法的参数名与URI模板变量名一致,则在@PathVariable中就可以省略别名的定义。下面是一个简写的示例:提示:如果参数是一个非必须的,可选的项,则可以在@PathVariable中设置require = false@RequestParam@RequestParam注解用于将方法的参数与Web请求的传递的参数进行绑定。使用@RequestParam可以轻松的访问HTTP请求参数的值。下面是使用该注解的代码示例:该注解的其他属性配置与@PathVariable的配置相同,特别的,如果传递的参数为空,还可以通过defaultValue设置一个默认值。示例代码如下:@Controller@Controller是@Component注解的一个延伸,Spring 会自动扫描并配置被该注解标注的类。此注解用于标注Spring MVC的控制器。下面是使用此注解的示例代码:@RestController@RestController是在Spring 4.0开始引入的,这是一个特定的控制器注解。此注解相当于@Controller和@ResponseBody的快捷方式。当使用此注解时,不需要再在方法上使用@ResponseBody注解。下面是使用此注解的示例代码:@ModelAttribute通过此注解,可以通过模型索引名称来访问已经存在于控制器中的model。下面是使用此注解的一个简单示例:与@PathVariable和@RequestParam注解一样,如果参数名与模型具有相同的名字,则不必指定索引名称,简写示例如下:特别地,如果使用@ModelAttribute对方法进行标注,Spring会将方法的返回值绑定到具体的Model上。示例如下:在Spring调用具体的处理方法之前,被@ModelAttribute注解标注的所有方法都将被执行。@CrossOrigin@CrossOrigin注解将为请求处理类或请求处理方法提供跨域调用支持。如果我们将此注解标注类,那么类中的所有方法都将获得支持跨域的能力。使用此注解的好处是可以微调跨域行为。使用此注解的示例如下:@InitBinder@InitBinder注解用于标注初始化WebDataBinider 的方法,该方法用于对Http请求传递的表单数据进行处理,如时间格式化、字符串处理等。下面是使用此注解的示例:二、Spring Bean注解在本小节中,主要列举与Spring Bean相关的4个注解以及它们的使用方式。@ComponentScan@ComponentScan注解用于配置Spring需要扫描的被组件注解注释的类所在的包。可以通过配置其basePackages属性或者value属性来配置需要扫描的包路径。面试宝典:https://www.yoodb.comvalue属性是basePackages的别名。此注解的用法如下:@Component@Component注解用于标注一个普通的组件类,它没有明确的业务范围,只是通知Spring被此注解的类需要被纳入到Spring Bean容器中并进行管理。此注解的使用示例如下:@Service@Service注解是@Component的一个延伸(特例),它用于标注业务逻辑类。与@Component注解一样,被此注解标注的类,会自动被Spring所管理。下面是使用@Service注解的示例:@Repository@Repository注解也是@Component注解的延伸,与@Component注解一样,被此注解标注的类会被Spring自动管理起来,@Repository注解用于标注DAO层的数据持久化类。此注解的用法如下:三、Spring Dependency Inject与Bean Scops注解Spring DI注解@DependsOn@DependsOn注解可以配置Spring IoC容器在初始化一个Bean之前,先初始化其他的Bean对象。下面是此注解使用示例代码:@Bean@Bean注解主要的作用是告知Spring,被此注解所标注的类将需要纳入到Bean管理工厂中。@Bean注解的用法很简单,在这里,着重介绍@Bean注解中initMethod和destroyMethod的用法。示例如下:Scops注解@Scope@Scope注解可以用来定义@Component标注的类的作用范围以及@Bean所标记的类的作用范围。@Scope所限定的作用范围有:singleton、prototype、request、session、globalSession或者其他的自定义范围。这里以prototype为例子进行讲解。当一个Spring Bean被声明为prototype(原型模式)时,在每次需要使用到该类的时候,Spring IoC容器都会初始化一个新的改类的实例。在定义一个Bean时,可以设置Bean的scope属性为prototype:scope=“prototype”,也可以使用@Scope注解设置,如下:@Scope(value=ConfigurableBeanFactory.SCOPE_PROPTOTYPE)下面将给出两种不同的方式来使用@Scope注解,示例代码如下:@Scope 单例模式当@Scope的作用范围设置成Singleton时,被此注解所标注的类只会被Spring IoC容器初始化一次。在默认情况下,Spring IoC容器所初始化的类实例都为singleton。同样的原理,此情形也有两种配置方式,示例代码如下:四、容器配置注解@Autowired@Autowired注解用于标记Spring将要解析和注入的依赖项。此注解可以作用在构造函数、字段和setter方法上。作用于构造函数下面是@Autowired注解标注构造函数的使用示例:作用于setter方法下面是@Autowired注解标注setter方法的示例代码:作用于字段@Autowired注解标注字段是最简单的,只需要在对应的字段上加入此注解即可,示例代码如下:@Primary当系统中需要配置多个具有相同类型的bean时,@Primary可以定义这些Bean的优先级。下面将给出一个实例代码来说明这一特性:输出结果:thisissendDingDingmethodmessage.@PostConstruct与@PreDestroy值得注意的是,这两个注解不属于Spring,它们是源于JSR-250中的两个注解,位于common-annotations.jar中。@PostConstruct注解用于标注在Bean被Spring初始化之前需要执行的方法。@PreDestroy注解用于标注Bean被销毁前需要执行的方法。下面是具体的示例代码:@Qualifier当系统中存在同一类型的多个Bean时,@Autowired在进行依赖注入的时候就不知道该选择哪一个实现类进行注入。此时,我们可以使用@Qualifier注解来微调,帮助@Autowired选择正确的依赖项。另外,推荐公众 号Java精选,回复java面试,获取资料。下面是一个关于此注解的代码示例:五、Spring Boot注解@SpringBootApplication@SpringBootApplication注解是一个快捷的配置注解,在被它标注的类中,可以定义一个或多个Bean,并自动触发自动配置Bean和自动扫描组件。此注解相当于@Configuration、@EnableAutoConfiguration和@ComponentScan的组合。在Spring Boot应用程序的主类中,就使用了此注解。示例代码如下:@SpringBootApplication publicclassApplication{ publicstaticvoidmain(String[]args){ SpringApplication.run(Application.class,args); } }@EnableAutoConfiguration@EnableAutoConfiguration注解用于通知Spring,根据当前类路径下引入的依赖包,自动配置与这些依赖包相关的配置项。@ConditionalOnClass与@ConditionalOnMissingClass这两个注解属于类条件注解,它们根据是否存在某个类作为判断依据来决定是否要执行某些配置。下面是一个简单的示例代码:@Configuration @ConditionalOnClass(DataSource.class) classMySQLAutoConfiguration{ //... }@ConditionalOnBean与@ConditionalOnMissingBean这两个注解属于对象条件注解,根据是否存在某个对象作为依据来决定是否要执行某些配置方法。示例代码如下:@Bean @ConditionalOnBean(name="dataSource") LocalContainerEntityManagerFactoryBeanentityManagerFactory(){ //... } @Bean @ConditionalOnMissingBean publicMyBeanmyBean(){ //... }@ConditionalOnProperty@ConditionalOnProperty注解会根据Spring配置文件中的配置项是否满足配置要求,从而决定是否要执行被其标注的方法。示例代码如下:@Bean @ConditionalOnProperty(name="alipay",havingValue="on") Alipayalipay(){ returnnewAlipay(); }@ConditionalOnResource此注解用于检测当某个配置文件存在使,则触发被其标注的方法,下面是使用此注解的代码示例:@ConditionalOnResource(resources="classpath:website.properties") PropertiesaddWebsiteProperties(){ //... }@ConditionalOnWebApplication与@ConditionalOnNotWebApplication这两个注解用于判断当前的应用程序是否是Web应用程序。如果当前应用是Web应用程序,则使用Spring WebApplicationContext,并定义其会话的生命周期。下面是一个简单的示例:@ConditionalOnWebApplication HealthCheckControllerhealthCheckController(){ //... }@ConditionalExpression此注解可以让我们控制更细粒度的基于表达式的配置条件限制。当表达式满足某个条件或者表达式为真的时候,将会执行被此注解标注的方法。@Bean @ConditionalException("${localstore}&&${local=='true'}") LocalFileStorestore(){ //... }@Conditional@Conditional注解可以控制更为复杂的配置条件。在Spring内置的条件控制注解不满足应用需求的时候,可以使用此注解定义自定义的控制条件,以达到自定义的要求。下面是使用该注解的简单示例:@Conditioanl(CustomConditioanl.class) CustomPropertiesaddCustomProperties(){ //... }总结本次课程总结了Spring Boot中常见的各类型注解的使用方式,让大家能够统一的对Spring Boot常用注解有一个全面的了解。
  • [技术干货] 定时任务之-------Elastic-Job
    Elastic-Job 是一个基于 Quartz 的分布式任务调度框架,它提供了弹性扩容、任务分片、失效转移等特性,适用于大规模分布式任务调度场景。本文将介绍 Elastic-Job 的主要特点、使用方法以及在实际项目中的应用场景和优势。 Elastic-Job 的主要特点 分布式任务调度:Elastic-Job 支持将任务分布到多个节点上执行,实现任务的高可用性和负载均衡。任务调度中心负责管理任务和分发任务到各个执行节点,保证任务的可靠执行。 弹性扩容和缩容:Elastic-Job 具有弹性伸缩的能力,可以根据任务负载自动增加或减少执行节点的数量,以应对不同规模的任务调度需求。 失败转移和错过执行策略:当任务执行失败或错过执行时间时,Elastic-Job 可以根据配置的策略进行相应的处理,如重试执行、跳过执行或报警通知等。 实时监控和统计:Elastic-Job 提供了丰富的监控和统计功能,可以实时查看任务的执行情况、节点的状态和任务的执行日志,方便进行故障排查和性能优化。 任务分片和并行执行:Elastic-Job 支持将一个任务分成多个独立的子任务进行并行执行,提高任务执行的效率和吞吐量。 使用 Elastic-Job 进行分布式任务调度 定义任务:编写一个实现 ElasticJob 接口的任务类,实现具体的任务逻辑。  public class MySimpleJob implements SimpleJob {      @Override     public void execute(ShardingContext shardingContext) {         System.out.println(String.format("------ Thread ID: %s, Sharding Item: %s, Time: %s ------",                 Thread.currentThread().getId(), shardingContext.getShardingItem(), new SimpleDateFormat("HH:mm:ss").format(new Date())));     } } 配置任务:通过配置文件或编程方式指定任务的调度策略、触发器、任务参数等信息。 xml <!-- 配置任务调度中心 --> <bean id="jobScheduler" class="io.elasticjob.lite.spring.job.SpringJobScheduler">     <constructor-arg ref="registryCenter" />     <constructor-arg>         <bean class="io.elasticjob.lite.job.config.JobCoreConfiguration" c:init-method="init" p:name="mySimpleJob" p:shardingTotalCount="3" p:cron="0/5 * * * * ?" />     </constructor-arg>     <constructor-arg>         <bean class="io.elasticjob.lite.job.config.simple.SimpleJobConfiguration" c:init-method="init" p:jobClass="com.example.MySimpleJob" />     </constructor-arg> </bean> 注册任务:将任务注册到任务调度中心,包括任务的名称、描述和执行类等信息。 @Bean(initMethod = "init") public ZookeeperRegistryCenter registryCenter() {     ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration("localhost:2181", "elastic-job-example");     return new ZookeeperRegistryCenter(zookeeperConfiguration); }  @Bean public MySimpleJob mySimpleJob() {     return new MySimpleJob(); } @Bean public JobScheduler jobScheduler(final MySimpleJob simpleJob, final ZookeeperRegistryCenter regCenter) {     return new SpringJobScheduler(simpleJob, regCenter, getLiteJobConfiguration(simpleJob.getClass(), "0/5 * * * * ?", 3, "0=Beijing,1=Shanghai", "simpleJob")); } private LiteJobConfiguration getLiteJobConfiguration(final Class<? extends SimpleJob> jobClass, final String cron, final int shardingTotalCount, final String shardingItemParameters, final String jobParameter) {     return LiteJobConfiguration.newBuilder(new SimpleJobConfiguration(JobCoreConfiguration.newBuilder(             jobClass.getName(), cron, shardingTotalCount).jobParameter(jobParameter).shardingItemParameters(shardingItemParameters).build(), jobClass.getCanonicalName())).overwrite(true).build(); } 启动任务调度中心:启动任务调度中心,它负责管理任务和分发任务到执行节点。 启动执行节点:启动执行节点,它负责接收任务调度中心分发的任务,并执行任务逻辑。 Elastic-Job 在实际项目中的应用场景 Elastic-Job 在许多实际项目中都有广泛的应用场景,特别适用于大规模分布式任务调度和处理场景。例如电商网站的订单处理、金融系统的批量处理、数据仓库的数据同步等。 Elastic-Job 的优势和局限性 优势:Elastic-Job 提供了灵活的分布式任务调度能力,简化了任务调度的开发和管理,提高了任务执行的可靠性和效率。 局限性:对于一些特定的任务调度需求,可能需要深入了解 Elastic-Job 的特性和配置,学习曲线较陡。 
  • [技术干货] 定时任务之-------Quartz
     一、Quartz 的概述 什么是 Quartz? Quartz 是一个开源的 Java 定时任务调度框架,由 Terracotta 公司开发并于2009年成为 Apache 顶级项目。它允许开发人员在 Java 应用程序中创建和管理各种类型的定时任务,并提供了丰富的功能和配置选项。 Quartz 的主要特点 灵活的任务调度:Quartz 支持基于日历的任务调度,可以按照指定的时间表执行任务,如每天、每周或每月。它还支持 cron 表达式,允许更灵活地定义任务执行时间。 分布式任务调度:Quartz 提供了集群支持,可以在多个节点上运行任务调度程序。这样可以实现高可用性和负载均衡,确保任务的可靠执行。 错过执行策略:当一个任务错过了预定的执行时间时,Quartz 可以根据配置的策略来处理。常见的策略包括立即执行、延迟执行或忽略该次执行。 任务持久化:Quartz 支持将任务信息持久化到数据库中,以便在应用程序重启后能够恢复任务状态。这对于长时间运行的任务或需要跟踪任务执行历史的场景非常有用。 监听器和触发器:Quartz 提供了丰富的监听器和触发器机制,可以在任务执行前后进行各种操作,如记录日志、发送通知或触发其他任务。 二、使用 Quartz 进行定时任务调度 创建 Job 类 在使用 Quartz 进行任务调度时,首先需要创建一个实现 org.quartz.Job 接口的 Job 类。该类定义了具体任务的执行逻辑,可以根据业务需求编写相应的逻辑代码。 创建 Trigger 对象 Trigger 对象用于指定任务的执行时间和频率。Quartz 提供了两种常用的 Trigger 实现:SimpleTrigger 和 CronTrigger。SimpleTrigger 允许指定任务的开始时间和重复间隔时间,而 CronTrigger 则可以使用 cron 表达式更灵活地定义任务执行时间。 创建 Scheduler 对象 Scheduler 是 Quartz 的核心组件,负责管理和调度任务。通过 StdSchedulerFactory 可以获取一个 Scheduler 实例,用于注册和启动任务。 将 Job 和 Trigger 注册到 Scheduler 中 使用 scheduler.scheduleJob() 方法将 Job 和 Trigger 关联起来,并注册到 Scheduler 中。 启动 Scheduler 调用 scheduler.start() 方法启动任务调度器,开始执行任务。 三、Quartz 在实际项目中的应用场景 Quartz 在许多实际项目中都有广泛的应用场景,以下是一些常见的应用场景: 数据清理和备份:定时清理数据库中的过期数据或执行数据备份操作。 缓存刷新:定时刷新缓存,保证数据的时效性。 报表生成:定时生成各类报表,如每日销售报表、月度统计报表等。 邮件发送:定时发送邮件,如每日汇总邮件、生日祝福邮件等。 系统监控:定时检查系统状态,如服务器性能监控、日志文件分析等。 四、Quartz 的优势和局限性 优势: 灵活性:Quartz 提供了丰富的配置选项和高级特性,可以满足各种复杂的任务调度需求。 可靠性:Quartz 支持任务的持久化和集群部署,保证任务执行的可靠性和高可用性。 扩展性:Quartz 提供了丰富的 API 和插件机制,可以方便地扩展和自定义功能。 局限性: 学习成本:Quartz 的学习曲线较陡,需要一定的时间和经验来熟悉其概念和使用方法。 复杂性:在处理一些复杂的任务调度需求时,可能需要编写复杂的代码逻辑和配置文件。 
  • [技术干货] 定时任务之-------Spring Task
    Spring Task 是一个强大而灵活的框架,能够帮助开发人员轻松地管理和调度定时任务。无论是通过注解驱动还是编程式方式,Spring Task 提供了简单易用的接口和功能,使得定时任务的实现变得简单而高效。本文将介绍 Spring Task 的基本用法,并探讨一些高级特性,以帮助读者更好地理解和应用该框架。  一、使用注解驱动的 Spring Task 启用注解驱动的 Spring Task 要使用注解驱动的 Spring Task,我们需要在 Spring 配置文件中启用相应的功能。可以通过在配置文件中添加 <task:annotation-driven /> 来启用注解驱动的 Spring Task。这样一来,我们就可以在需要执行定时任务的方法上使用 @Scheduled 注解来标记。 定时任务的基本配置 在使用注解驱动的 Spring Task 时,我们可以使用 @Scheduled 注解来标记一个方法作为定时任务。该注解支持多种配置方式,包括 cron 表达式、固定延迟和固定频率。 cron 表达式:可以指定任务执行的时间表达式,以便更加精确地控制任务的执行时机。 @Scheduled(cron = "0 0 1 * * ?") public void runAtOneAM() {     // 执行具体的定时任务逻辑 } 固定延迟:表示任务执行完成后固定延迟多久再次执行。 @Scheduled(fixedDelay = 5000) public void runEveryFiveSeconds() {     // 每隔 5 秒执行一次任务 } 固定频率:表示任务开始执行后固定频率执行。 @Scheduled(fixedRate = 5000) public void runEveryFiveSeconds() {     // 每隔 5 秒执行一次任务 } 通过简单的注解配置,我们可以方便地实现各种定时任务,并根据需求灵活地调整任务的执行方式。 二、使用编程式的 Spring Task 除了使用注解驱动的方式,Spring Task 还提供了编程式的方式来管理和调度定时任务。我们可以通过实现 Runnable 接口或继承 TimerTask 类来定义定时任务,并将它们注册到 TaskScheduler 中。 以下是一个使用编程式方式实现定时任务的示例代码: public class MyTask implements Runnable {       @Override     public void run() {         // 执行具体的定时任务逻辑     } }  ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); TaskScheduler scheduler = new ConcurrentTaskScheduler(executor); scheduler.scheduleAtFixedRate(new MyTask(), 5000); 在这个示例中,我们创建了一个 ScheduledExecutorService 线程池,并使用它来创建 ConcurrentTaskScheduler 对象。然后,我们调用 scheduleAtFixedRate 方法来注册定时任务,指定了任务对象和重复执行的时间间隔。 通过编程式的方式,我们可以更加灵活地管理和调度定时任务,尤其适用于一些复杂的场景。 三、高级特性 除了基本的用法之外,Spring Task 还提供了一些高级特性,以帮助开发人员更好地管理和调度定时任务。 异步执行:Spring Task 支持在单独的线程中执行定时任务,从而不阻塞应用程序的其他操作。 并发控制:如果一个任务的执行时间超过了下一次执行的间隔,Spring Task 可以灵活地处理并发执行的情况,确保任务按照预期的间隔执行。 定时任务监听:通过实现 TaskScheduler 的接口,我们可以监听定时任务的执行情况,并进行相应的处理。 这些高级特性使得 Spring Task 更加强大和灵活,能够满足各种复杂的定时任务需求。 
  • [技术干货] Spring高手之路-@Autowired和@Resource注解异同点-转载
     概述 @Autowired 和 @Resource 是在 Java 开发中用于实现依赖注入的注解。  @Autowired 是 Spring 框架提供的注解,用于自动装配(自动注入)依赖。通过在需要注入依赖的字段、构造方法或者方法上使用 @Autowired 注解,Spring 容器会自动寻找匹配类型的 Bean,并将其注入到被注解的位置。如果存在多个满足条件的 Bean,则可以使用 @Qualifier 注解指定具体的 Bean。  @Resource 是 JavaEE 规范提供的注解,也可用于依赖注入。它可以根据名称或者类型进行依赖注入。当使用名称进行依赖注入时,@Resource 注解会根据指定的名称去查找匹配的 Bean 进行注入;当使用类型进行依赖注入时,@Resource 注解会使用类型去查找匹配的 Bean 进行注入。  相同点 1.都可以实现依赖注入 通过注解将需要的Bean自动注入到目标类中。  2.都可以用于注入任意类型的Bean 包括类、接口、原始类型、数组等。  3.都支持通过名称、类型匹配进行注入 @Autowired 注解默认按照类型匹配,而 @Resource 注解默认按照名称匹配  @Autowired private Bean beanA; @Resource private Bean beanB; 在Spring容器中这两个注解功能基本是等价的,都可以将bean注入到对应的字段中。  不同点 虽然功能上看起来基本相同还是存在区别的下面从几个不同方面分析  1.来源不同。 @Autowired 是 Spring 框架提供的注解。  @Resource 是 JavaEE(现在的 JakartaEE)规范中定义的注解。  2.包含的属性不同 @Autowired 只包含一个参数:required,表示是否开启自动注入,默认是true。  @Resource 包含七个参数,其中最重要的两个参数是:name 和 type。  3.匹配方式(装配顺序)不同。 @Autowired 默认先按照类型进行自动装配,再是根据名称的方式。意思就是先在Spring容器中找以Bean为类型的Bean实例,如果找不到或者找到多个bean,则会进一步通过字段名称来找。当有多个同类型的 Bean 存在时,也可以通过 @Qualifier 注解指定具体的 Bean。  @Component public class UserService {     @Autowired     @Qualifier("userRepository")//如果有多个同类型的Bean,可以使用@Qualifier注解指定具体的Bean     private UserRepository userRepository;     // ... }  @Resource 和@Autowired恰好相反,先是按照名称方式,然后再是按照类型方式;当然,我们也可以通过注解中的参数显示指定通过哪种方式。如果有多个同名的Bean,可以使用@Resource注解的name属性指定具体的Bean  默认使用  @Component public class UserService {     @Resource//不指定任何属性     private UserRepository userRepository;     // ... }  指定name  @Component public class UserService {     @Resource(name = "userRepository")//使用name属性指定具体的Bean     private UserRepository userRepository;     // ... }  指定type  @Component public class UserService {     @Resource(type = UserRepository.class)//使用type属性指定Bean类型     private UserRepository userRepository;     // ... }  指定name和type  @Component public class UserService {     @Resource(type = "UserRepository.class",name = "userRepository")//使用type属性指定Bean类型,name指定Bean名称     private UserRepository userRepository;     // ... }  4.支持的注入对象类型不同 @Autowired 可以注入任何类型的对象,只要 Spring 容器中存在该类型的 Bean。  @Resource 注解可以用于注入 JNDI 名称(JNDI名称可以是任何字符串,但通常使用具有描述性的名称来标识资源。在应用程序中,可以使用JNDI名称来查找和绑定对象)或者默认按照名称匹配的 Bean  5.应用地方不同 @Autowired能够用在:构造器、方法、参数、成员变量和注解上  @Resource能用在:类、成员变量和方法上。 ———————————————— 版权声明:本文为CSDN博主「mi9688」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_62262918/article/details/135291547 
  • [技术干货] Spring Task(定时任务)框架
    一、Spring Task介绍Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。应用场景:信用卡每月还款提醒银行贷款每月还款提醒火车票售票系统处理未支付订单入职纪念日为用户发送通知等等...(只要是需要定时处理当达到场景都可以使用Spring Task)二、cron表达式1.cron表达式介绍cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发时间。 构成规则:分成6或7个域(位置),由空格分隔开,每个域代表一个含义 每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选) 举例: 其中日和周是两个相互对立的,当一个定义为数字后,另一个定义为 ? 2.cron表达式在线生成器网址:cid:link_0 这里需要了解一些cron的相关语法规范。特殊字符介绍: 逗号(,):指定一个值列表,例如使用在月域上1,4,5,7表示1月、4月、5月和7月横杠(-):指定一个范围,例如在时域上3-6表示3点到6点(即3点、4点、5点、6点)星号(*):表示这个域上包含所有合法的值。例如,在月份域上使用星号意味着每个月都会触发斜线(/):表示递增,例如使用在秒域上0/15表示每15秒问号(?):只能用在日和周域上,但是不能在这两个域上同时使用。表示不指定井号(#):只能使用在周域上,用于指定月份中的第几周的哪一天,例如6#3,意思是某月的第三个周五 (6=星期五,3意味着月份中的第三周)L:某域上允许的最后一个值。只能使用在日和周域上。当用在日域上,表示的是在月域上指定的月份的最后一天。用于周域上时,表示周的最后一天,就是星期六W:W 字符代表着工作日 (星期一到星期五),只能用在日域上,它用来指定离指定日的最近的一个工作日三、fixedDelay它的间隔时间是根据上次的任务结束的时候开始计时的,只要盯紧上一次执行结束的时间即可,跟任务逻辑的执行时间无关,两个轮次的间隔距离是固定的。 四、fixedRate这个相对难以理解一些。在理想情况下,下一次开始和上一次开始之间的时间间隔是一定的。但是默认情况下 Spring Boot 定时任务是单线程执行的。当下一轮的任务满足时间策略后任务就会加入队列,也就是说当本次任务开始执行时下一次任务的时间就已经确定了,由于本次任务的“超时”执行,下一次任务的等待时间就会被压缩甚至阻塞,看图就明白了。 五、initialDelay初始化延迟时间,也就是第一次延迟执行的时间。这个参数对 cron 属性无效,只能配合 fixedDelay 或 fixedRate 使用。如 @Scheduled(initialDelay=5000,fixedDelay = 1000) 表示第一次延迟 5000 毫秒执行,下一次任务在上一次任务结束后 1000 毫秒后执行。六、Spring Task的使用1.导入maven坐标spring-context代码如下(示例):<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency>2.启动类添加注解@EnableScheduling开启任务调度代码如下(示例):@SpringBootApplication @EnableTransactionManagement //开启注解方式的事务管理 @Slf4j @EnableCaching//开启缓存注解功能 @EnableScheduling //开启任务调度 public class SkyApplication { public static void main(String[] args) { SpringApplication.run(SkyApplication.class, args); log.info("server started"); } }3.自定义定时任务类举例代码:/** * 自定义定时任务类 */ @Component //当前类需要实例化,交给spring管理,所以加@Component @Slf4j public class MyTask { /** * 定时任务 每隔5秒触发一次 */ @Scheduled(cron = "0/5 * * * * ?") //指定任务什么时候触发 public void executeTask(){ log.info("定时任务开始执行:{}",new Date()); /** * 定时任务的一些任务逻辑 */ } }执行结果:
  • [技术干货] Nacos注册中心有几种调用方式?
    Spring Cloud Alibaba Nacos 作为近几年最热门的注册中心和配置中心,也被国内无数公司所使用,今天我们就来看下 Nacos 作为注册中心时,调用它的接口有几种方式?1.什么是注册中心?注册中心(Registry)是一种用于服务发现和服务注册的分布式系统组件。它是在微服务架构中起关键作用的一部分,用于管理和维护服务实例的信息以及它们的状态。注册中心充当了服务之间的中介和协调者,它的主要功能有以下这些:服务注册:服务提供者将自己的服务实例信息(例如 IP 地址、端口号、服务名称等)注册到注册中心。通过注册中心,服务提供者可以将自己的存在告知其他服务。服务发现:服务消费者通过向注册中心查询服务信息,获取可用的服务实例列表。通过注册中心,服务消费者可以找到并连接到需要调用的服务。健康检查与负载均衡:注册中心可以定期检查注册的服务实例的健康状态,并从可用实例中进行负载均衡,确保请求可以被正确地转发到可用的服务实例。动态扩容与缩容:在注册中心中注册的服务实例信息可以方便地进行动态的增加和减少。当有新的服务实例上线时,可以自动地将其注册到注册中心。当服务实例下线时,注册中心会将其从服务列表中删除。使用注册中心有以下优势和好处:服务自动发现和负载均衡:服务消费者无需手动配置目标服务的地址,而是通过注册中心动态获取可用的服务实例,并通过负载均衡算法选择合适的实例进行调用。服务弹性和可扩展性:新的服务实例可以动态注册,并在发生故障或需要扩展时快速提供更多的实例,从而提供更高的服务弹性和可扩展性。中心化管理和监控:注册中心提供了中心化的服务管理和监控功能,可以对服务实例的状态、健康状况和流量等进行监控和管理。降低耦合和提高灵活性:服务间的通信不再直接依赖硬编码的地址,而是通过注册中心进行解耦,使得服务的部署和变更更加灵活和可控。常见的注册中心包括 ZooKeeper、Eureka、Nacos 等。这些注册中心可以作为微服务架构中的核心组件,用于实现服务的自动发现、负载均衡和动态扩容等功能。2.方法概述当 Nacos 中注册了 Restful 接口时(一种软件架构风格,它是基于标准的 HTTP 协议和 URI 的一组约束和原则),其调用方式主要有以下两种:使用 RestTemplate + Spring Cloud LoadBalancer使用 OpenFeign + Spring Cloud LoadBalancer3.RestTemplate+LoadBalancer调用此方案的实现有以下 3 个关键步骤:添加依赖:nacos + loadbalancer设置配置文件编写调用代码具体实现如下。3.1 添加依赖<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>3.2 设置配置文件spring: application: name: nacos-discovery-business cloud: nacos: discovery: server-addr: localhost:8848 username: nacos password: nacos register-enabled: false3.3 编写调用代码此步骤又分为以下两步:给 RestTemplate 增加 LoadBalanced 支持使用 RestTemplate 调用接口3.3.1 RestTemplate添加LoadBalanced在 Spring Boot 启动类上添加“@EnableDiscoveryClient”注解,并使用“@LoadBalanced”注解替换 IoC 容器中的 RestTemplate,具体实现代码如下:import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication @EnableDiscoveryClient public class BusinessApplication { @LoadBalanced @Bean public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(BusinessApplication.class, args); } }3.3.2 使用RestTemplateimport org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController @RequestMapping("/business") public class BusinessController2 { @Autowired private RestTemplate restTemplate; @RequestMapping("/getnamebyid") public String getNameById(Integer id){ return restTemplate.getForObject("http://nacos-discovery-demo/user/getnamebyid?id="+id, String.class); } }4.OpenFeign+LoadBalancer调用此步骤又分为以下 5 步:添加依赖:nacos + openfeign + loadbalancer设置配置文件开启 openfeign 支持编写 service 代码调用 service 代码具体实现如下。4.1 添加依赖<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>4.2 设置配置文件spring: application: name: nacos-discovery-business cloud: nacos: discovery: server-addr: localhost:8848 username: nacos password: nacos register-enabled: false4.3 开启OpenFeign在 Spring Boot 启动类上添加 @EnableFeignClients 注解。4.4 编写Serviceimport org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @Service @FeignClient(name = "nacos-producer") // name 为生产者的服务名 public interface UserService { @RequestMapping("/user/getinfo") // 调用生产者的接口 String getInfo(@RequestParam String name); }4.5 调用Serviceimport com.example.consumer.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class OrderController { @Autowired private UserService userService; @RequestMapping("/order") public String getOrder(@RequestParam String name){ return userService.getInfo(name); } }5.获取本文源码因平台不能上传附件,所以想要获取本文完整源码,请联系我:gg_stone,备注:Nacos 源码,不然不予通过。6.版本说明本文案例基于以下版本:JDK 17Spring Boot 3.xSpring Cloud Alibaba 2022.0.0.0Nacos 2.2.37.小结注册中心作为微服务中不可或缺的重要组件,在微服务中充当着中介和协调者的作用。而 Nacos 作为近几年来,国内最热门的注册中心,其 Restf 接口调用有两种方式:RestTemplate + LoadBalancer 和 OpenFeign + LoadBalancer,开发者可以根据自己的实际需求,选择相应的调用方式。转载自https://www.cnblogs.com/vipstone/p/17797163.html
  • [技术干货] 万字详谈SpringBoot多数据源以及事务处理【转】
    转自 cid:link_0背景在高并发的项目中,单数据库已无法承载大数据量的访问,因此需要使用多个数据库进行对数据的读写分离,此外就是在微服化的今天,我们在项目中可能采用各种不同存储,因此也需要连接不同的数据库,居于这样的背景,这里简单分享实现的思路以及实现方案。如何实现多数据源实现思路有两种,一种是通过配置多个SqlSessionFactory实现多数据源;另外一种是通过Spring提供的AbstractRoutingDataSource抽象了一个DynamicDataSource实现动态切换数据源;实现方案准备 采用Spring Boot2.7.8框架,数据库Mysql,ORM框架采用Mybatis,整个Maven依赖如下: <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <spring-boot.version>2.7.8</spring-boot.version> <mysql-connector-java.version>5.1.46</mysql-connector-java.version> <mybatis-spring-boot-starter.version>2.0.0</mybatis-spring-boot-starter.version> <mybatis.version>3.5.1</mybatis.version><dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql-connector-java.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis-spring-boot-starter.version}</version> </dependency> </dependencies> </dependencyManagement>指定数据源操作指定目录XML文件 该种方式需要操作的数据库的Mapper层和Dao层分别建立一个文件夹,分包放置,整体项目结构如下图:Maven依赖如下: org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test com.zaxxer HikariCP 4.0.3 mysql mysql-connector-java mysql mysql-connector-java org.mybatis mybatis org.mybatis.spring.boot mybatis-spring-boot-starter junit junit test Yaml文件 spring: datasource: user: jdbc-url: jdbc:mysql://127.0.0.1:3306/study_user?useSSL=false&useUnicode=true&characterEncoding=UTF-8 username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource #hikari连接池配置 hikari: #pool name pool-name: user #最小空闲连接数 minimum-idle: 5 #最大连接池 maximum-pool-size: 20 #链接超时时间 3秒 connection-timeout: 3000 # 连接测试query connection-test-query: SELECT 1 soul: jdbc-url: jdbc:mysql://127.0.0.1:3306/soul?useSSL=false&useUnicode=true&characterEncoding=UTF-8 username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource #hikari连接池配置 hikari: #pool name pool-name: soul #最小空闲连接数 minimum-idle: 5 #最大连接池 maximum-pool-size: 20 #链接超时时间 3秒 connection-timeout: 3000 # 连接测试query connection-test-query: SELECT 1 不同库的Mapper指定不同的SqlSessionFactory 针对不同的库分别放置对用不同的SqlSessionFactory @Configuration @MapperScan(basePackages = "org.datasource.demo1.usermapper", sqlSessionFactoryRef = "userSqlSessionFactory") public class UserDataSourceConfiguration {public static final String MAPPER_LOCATION = "classpath:usermapper/*.xml"; @Primary @Bean("userDataSource") @ConfigurationProperties(prefix = "spring.datasource.user") public DataSource userDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "userTransactionManager") @Primary public PlatformTransactionManager userTransactionManager(@Qualifier("userDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Primary @Bean(name = "userSqlSessionFactory") public SqlSessionFactory userSqlSessionFactory(@Qualifier("userDataSource") DataSource dataSource) throws Exception { final SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(UserDataSourceConfiguration.MAPPER_LOCATION)); return sessionFactoryBean.getObject(); }} @Configuration @MapperScan(basePackages = "org.datasource.demo1.soulmapper", sqlSessionFactoryRef = "soulSqlSessionFactory") public class SoulDataSourceConfiguration {public static final String MAPPER_LOCATION = "classpath:soulmapper/*.xml"; @Bean("soulDataSource") @ConfigurationProperties(prefix = "spring.datasource.soul") public DataSource soulDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "soulTransactionManager") public PlatformTransactionManager soulTransactionManager(@Qualifier("soulDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean(name = "soulSqlSessionFactory") public SqlSessionFactory soulSqlSessionFactory(@Qualifier("soulDataSource") DataSource dataSource) throws Exception { final SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean(); sessionFactoryBean.setDataSource(dataSource); sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(SoulDataSourceConfiguration.MAPPER_LOCATION)); return sessionFactoryBean.getObject(); }} 使用 @Service public class AppAuthService {@Autowired private AppAuthMapper appAuthMapper; @Transactional(rollbackFor = Exception.class) public int getCount() { int a = appAuthMapper.listCount(); int b = 1 / 0; return a; }}@SpringBootTest @RunWith(SpringRunner.class) public class TestDataSource {@Autowired private AppAuthService appAuthService; @Autowired private SysUserService sysUserService; @Test public void test_dataSource1(){ int b=sysUserService.getCount(); int a=appAuthService.getCount(); }} 总结 此种方式使用起来分层明确,不存在任何冗余代码,不足地方就是每个库都需要对应一个配置类,该配置类中实现方式都基本类似,该种解决方案每个配置类中都存在事务管理器,因此不需要单独再去额外的关注。 AOP+自定义注解 关于采用Spring AOP方式实现原理就是把多个数据源存储在一个 Map中,当需要使用某个数据源时,从 Map中获取此数据源进行处理。AbstractRoutingDataSource 在Spring中提供了AbstractRoutingDataSource来实现此功能,继承AbstractRoutingDataSource类并覆写其determineCurrentLookupKey()方法就可以完成数据源切换,该方法只需要返回数据源key即可,也就是存放数据源的Map的key,接下来我们来看一下AbstractRoutingDataSource整体的继承结构,看他是如何做到的。在整体的继承结构上我们会发现AbstractRoutingDataSource最终是继承于DataSource,因此当我们继承AbstractRoutingDataSource是我们自身也是一个数据源,对于数据源必然有连接数据库的动作,如下代码: public Connection getConnection() throws SQLException { return this.determineTargetDataSource().getConnection(); }public Connection getConnection(String username, String password) throws SQLException { return this.determineTargetDataSource().getConnection(username, password); } 只是AbstractRoutingDataSource的getConnection()方法里实际是调用determineTargetDataSource()返回的数据源的getConnection()方法。 protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = this.determineCurrentLookupKey(); DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; }if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } else { return dataSource; } } 该方法通过determineCurrentLookupKey()方法获取一个key,通过key从resolvedDataSources中获取数据源DataSource对象。determineCurrentLookupKey()是个抽象方法,需要继承AbstractRoutingDataSource的类实现;而resolvedDataSources是一个Map<Object, DataSource>,里面应该保存当前所有可切换的数据源,接下来我们来聊聊实现,我们首先来看下目录,与分包的不同的是将所有的Mapper文件都放到一起,其他Maven依赖以及配置文件都保持一致。DataSourceType 该枚举用来存放数据源的名称, public enum DataSourceType {USERDATASOURCE("userDataSource"), SOULDATASOURCE("soulDataSource"); private String name; DataSourceType(String name) { this.name=name; } public String getName() { return name; } public void setName(String name) { this.name = name; }} DynamicDataSourceConfiguration 通过读取配置文件中的数据源配置信息,创建数据连接,将多个数据源放入Map中,注入到容器中: @Configuration @MapperScan(basePackages = "org.datasource.demo2.mapper") public class DynamicDataSourceConfiguration {@Primary @Bean(name = "userDataSource") @ConfigurationProperties(prefix = "spring.datasource.user") public DataSource userDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "soulDataSource") @ConfigurationProperties(prefix = "spring.datasource.soul") public DataSource soulDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "dynamicDataSource") public DynamicDataSource DataSource(@Qualifier("userDataSource") DataSource userDataSource, @Qualifier("soulDataSource") DataSource soulDataSource) { //targetDataSource 集合是我们数据库和名字之间的映射 Map<Object, Object> targetDataSource = new HashMap<>(); targetDataSource.put(DataSourceType.USERDATASOURCE.getName(), userDataSource); targetDataSource.put(DataSourceType.SOULDATASOURCE.getName(), soulDataSource); DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSource); //设置默认对象 dataSource.setDefaultTargetDataSource(userDataSource); return dataSource; } @Bean(name = "sqlSessionFactory") public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setTransactionFactory(new MultiDataSourceTransactionFactory()); bean.setDataSource(dynamicDataSource); //设置我们的xml文件路径 bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources( "classpath*:mapper/*.xml")); return bean.getObject(); }} DataSourceContext DataSourceContext使用ThreadLocal存放当前线程使用的数据源类型信息; public class DataSourceContext {private final static ThreadLocal<String> LOCAL_DATASOURCE = new ThreadLocal<>(); public static void set(String name) { LOCAL_DATASOURCE.set(name); } public static String get() { return LOCAL_DATASOURCE.get(); } public static void remove() { LOCAL_DATASOURCE.remove(); }} DynamicDataSource DynamicDataSource继承AbstractRoutingDataSource,重写determineCurrentLookupKey()方法,可以选择对应Key; public class DynamicDataSource extends AbstractRoutingDataSource {@Override protected Object determineCurrentLookupKey() { return DataSourceContext.get(); }} CurrentDataSource 定义数据源的注解; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface CurrentDataSource { DataSourceType value() default DataSourceType.USERDATASOURCE; } DataSourceAspect 定义切面切点,用来切换数据源, @Aspect @Order(-1) @Component public class DataSourceAspect {@Pointcut("@annotation(org.datasource.demo2.constant.CurrentDataSource)") public void dsPointCut() { } @Around("dsPointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); CurrentDataSource dataSource = method.getAnnotation(CurrentDataSource.class); if (Objects.nonNull(dataSource)) { System.out.println("切换数据源为" + dataSource.value().getName()); DataSourceContext.set(dataSource.value().getName()); } try { return point.proceed(); } finally { // 销毁数据源 在执行方法之后 System.out.println("销毁数据源" + dataSource.value().getName()); DataSourceContext.remove(); } }} 多数据源切换以后事务问题 Spring使用事务的方式有两种,一种是声明式事务,一种是编程式事务,我们讨论的都是关于声明式事务,这种方式很方便,也是大家常用的,这里为什么讨论这个问题,当我们想将不同库的表放在同一个事务使用的时候,这个是时候我们会报错,如下图:这部分也就是其他技术贴没讲解的部分,因此这里我们来补充一下这个话题,背过八股们的小伙伴都知道Spring事务是居于AOP实现,从这个角度很容易会理解到这个问题,当我们将两个Service方法放在同一个Transactional下的时候,这个代理对象就是当前类,因此导致数据源对象也是当前类下的DataSource,导致就出现表不存在问题,当Transactional分别放在不同Service的时候没有这种情况。 @Transactional(rollbackFor = Exception.class) public void update(){ sysUserMapper.updateSysUser("111"); appAuthService.update("111"); } 有没有办法解决这个问题呢,当然是有的,这里我就不一步一步去探讨源码问题,我就直接直捣黄龙,把问题本质说一下,在Spring事务管理中有一个核心类DataSourceTransactionManager,该类是Spring事务核心的默认实现,AbstractPlatformTransactionManager是整体的Spring事务实现模板类,整体的继承结构如下图,在方案一中,我们针对每个DataSourece都创建对应的DataSourceTransactionManager实现,也可以看出DataSourceTransactionManager就是管理我们整体的事务的,当我们配置了事物管理器以及拦截Service中的方法后,每次执行Service中方法前会开启一个事务,并且同时会缓存DataSource、SqlSessionFactory、Connection,因为DataSource、Conneciton都是从缓存中拿的,因此我们怎么切换数据源也没用,因此就出现表不存在的报错,具体源码可参考下面截图部分:看到这里我们大致明白了为什么会报错,那么我们该如何做才能实现这种情况呢?其实我们要做的事就是动态的根据DataSourceType获取不同的Connection,不从缓存中获取Connection。 解决方案 我们来自定义一个MultiDataSourceTransaction实现Mybatis的事务接口,使用Map存储Connection相关连接,所有事务都采用手动提交,之后将MultiDataSourceTransaction交给SpringManagedTransactionFactory处理。 public class MultiDataSourceTransaction implements Transaction {private final DataSource dataSource; private ConcurrentMap<String, Connection> concurrentMap; private boolean autoCommit; public MultiDataSourceTransaction(DataSource dataSource) { this.dataSource = dataSource; concurrentMap = new ConcurrentHashMap<>(); } @Override public Connection getConnection() throws SQLException { String databaseIdentification = DataSourceContext.get(); if (StringUtils.isEmpty(databaseIdentification)) { databaseIdentification = DataSourceType.USERDATASOURCE.getName(); } //获取数据源 if (!this.concurrentMap.containsKey(databaseIdentification)) { try { Connection conn = this.dataSource.getConnection(); autoCommit=false; conn.setAutoCommit(false); this.concurrentMap.put(databaseIdentification, conn); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Could bot get JDBC otherConnection", ex); } } return this.concurrentMap.get(databaseIdentification); } @Override public void commit() throws SQLException { for (Connection connection : concurrentMap.values()) { if (!autoCommit) { connection.commit(); } } } @Override public void rollback() throws SQLException { for (Connection connection : concurrentMap.values()) { connection.rollback(); } } @Override public void close() throws SQLException { for (Connection connection : concurrentMap.values()) { DataSourceUtils.releaseConnection(connection, this.dataSource); } } @Override public Integer getTimeout() throws SQLException { return null; }}public class MultiDataSourceTransactionFactory extends SpringManagedTransactionFactory { @Override public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) { return new MultiDataSourceTransaction(dataSource); } } 为什么可以这么做 在Mybatis自动装配式会将配置文件装配为Configuration对象,也就是在方案一种SqlSessionFactory配置的过程,其中SqlSessionFactoryBean类实现了InitializingBean接口,初始化后执行afterPropertiesSet()方法,在afterPropertiesSet()方法中会执行 BuildSqlSessionFactory() 方法生成一个SqlSessionFactory对象。在BuildSqlSessionFactory中,会创建SpringManagedTransactionFactory对象,该对象就是MyBatis跟 Spring的桥梁。在MapperScan自动扫描Mapper过程中,会通过ClassPathMapperScanner扫描器找到Mapper接口,封装成各自的BeanDefinition,然后循环遍历对Mapper的BeanDefinition修改beanClass为MapperFactoryBean。由于MapperFactoryBean实现了FactoryBean,在Bean生命周期管理时会调用getObject方法,通过JDK动态代理生成代理对象MapperProxy,Mapper接口请求的时候,执行MapperProxy代理类的invoke方法,执行的过程中通过SqlSessionFactory创建的SqlSession去调用Executor执行器,进行数据库操作。下图是SqlSession创建的整个过程:openSession方法是将Spring事务管理关联起来的核心代码,首先这里将通过 getTransactionFactoryFromEnvironment()方法获取TransactionFactory。这个操作会得到初始化时候注入的 SpringManagedTransactionFactory对象。然后将执行TransactionFactory#newTransaction() 方法,初始化 MyBatis的Transaction。这里通过Configuration.newExecutor()创建一个Executor,Configuration指定在Executor默认为Simple,因此这里会创建一个SimpleExecutor,并初始化Transaction属性。接下来我们来看下SimpleExecutor执行执行update方法时候执行prepareStatement方法,在prepareStatement方法中执行了getConnection方法,这里我们可以看到Connection获取过程,是通过Transaction获取的getConnection(),也就是通过之前注入的Transaction来获取Connection,这个Transaction就是SpringManagedTransaction,整体的时序图如下:在整个调用链过程中,我们看到在DataSourceUtils有我们熟悉的TransactionSynchronizationManager,在上面Spring事务的时候我们也提到这个类,在开始Spring事务以后就会把Connetion绑定到当前线程,在DataSourceUtils获取到的Connection对象就是Srping开启事务时候创建的对象,这样就保证了Spring Transaction中的Connection跟MyBatis中执行SQL语句用的Connection为同一个 Connection,也就可以通过Spring事务管理机制进行事务管理了。明白了整个流程,我们要做的事也就很简单,也就是每次切换DataSoure的同时获取最新的Connection,然后用一个Map对象来记录整个过程中的Connection,出现回滚这个Map对象里面Connection对象都回滚就可以了,然后将我们自定义的Transaction,委托给Spring在进行管理。 总结 采用AOP的方式是切换数据源已经非常好了,唯一不太好的地方就在于依然要手动去创建DataSource,每次增加都需要增加一个Bean,那有没有办法解决呢?当然是有的,让我们来更上一层楼,解放双手。 更上一层楼 ImportBeanDefinitionRegistrar ImportBeanDefinitionRegistrar接口是Spring提供一个扩展点,主要用来注册BeanDefinition,常见的第三方框架在集成Spring的时候,都会通过该接口,实现扫描指定的类,然后注册到Spring容器中。比如 Mybatis中的Mapper接口,SpringCloud中的Feignlient接口,都是通过该接口实现的自定义注册逻辑。 我们要做的事情就是通过ImportBeanDefinitionRegistrar帮助我们动态的将DataSource扫描的到容器中去,不在采用增加Bean的方式,整体代码如下: public class DynamicDataSourceBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {/** * 默认dataSource */ private DataSource defaultDataSource; /** * 数据源map */ private Map<String, DataSource> dataSourcesMap = new HashMap<>(); @Override public void setEnvironment(Environment environment) { initConfig(environment); } private void initConfig(Environment env) { //读取配置文件获取更多数据源 String dsNames = env.getProperty("spring.datasource.names"); for (String dsName : dsNames.split(",")) { HikariConfig hikariConfig = new HikariConfig(); hikariConfig.setPoolName(dsName); hikariConfig.setDriverClassName(env.getProperty("spring.datasource." + dsName.trim() + ".driver-class-name")); hikariConfig.setJdbcUrl(env.getProperty("spring.datasource." + dsName.trim() + ".jdbc-url")); hikariConfig.setUsername(env.getProperty("spring.datasource." + dsName.trim() + ".username")); hikariConfig.setPassword(env.getProperty("spring.datasource." + dsName.trim() + ".password")); hikariConfig.setConnectionTimeout(Long.parseLong(Objects.requireNonNull(env.getProperty("spring.datasource." + dsName.trim() + ".hikari.connection-timeout")))); hikariConfig.setMinimumIdle(Integer.parseInt(Objects.requireNonNull(env.getProperty("spring.datasource." + dsName.trim() + ".hikari.minimum-idle")))); hikariConfig.setMaximumPoolSize(Integer.parseInt(Objects.requireNonNull(env.getProperty("spring.datasource." + dsName.trim() + ".hikari.maximum-pool-size")))); hikariConfig.setConnectionInitSql("SELECT 1"); HikariDataSource dataSource = new HikariDataSource(hikariConfig); if (dataSourcesMap.size() == 0) { defaultDataSource = dataSource; } dataSourcesMap.put(dsName, dataSource); } } @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { Map<Object, Object> targetDataSources = new HashMap<Object, Object>(); //添加其他数据源 targetDataSources.putAll(dataSourcesMap); //创建DynamicDataSource GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClass(DynamicDataSource.class); beanDefinition.setSynthetic(true); MutablePropertyValues mpv = beanDefinition.getPropertyValues(); //defaultTargetDataSource 和 targetDataSources属性是 AbstractRoutingDataSource的两个属性Map mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource); mpv.addPropertyValue("targetDataSources", targetDataSources); //注册 registry.registerBeanDefinition("dataSource", beanDefinition); }} @Import @Import模式是向容器导入Bean是一种非常重要的方式,在注解驱动的Spring项目中,@Enablexxx的设计模式中有大量的使用,我们通过ImportBeanDefinitionRegistrar完成Bean的扫描,通过@Import导入到容器中,然后将EnableDynamicDataSource放入SpringBoot的启动项之上,到这里有没有感觉到茅塞顿开的感觉。 @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Import({DynamicDataSourceBeanDefinitionRegistrar.class}) public @interface EnableDynamicDataSource { } @SpringBootApplication @EnableAspectJAutoProxy @EnableDynamicDataSource public class DataSourceApplication {public static void main(String[] args) { SpringApplication.run(DataSourceApplication.class, args); }} DynamicDataSourceConfig 该类负责将Mapper扫描以及SpringFactory定义; @Configuration @MapperScan(basePackages = "org.datasource.demo3.mapper") public class DynamicDataSourceConfig {@Autowired private DataSource dynamicDataSource; @Bean(name = "sqlSessionFactory") public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setTransactionFactory(new MultiDataSourceTransactionFactory()); bean.setDataSource(dynamicDataSource); //设置我们的xml文件路径 bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources( "classpath*:mapper/*.xml")); return bean.getObject(); }} yaml 关于yaml部分我们增加了names定义,方便识别出来配置了几个DataSource,剩下的部分与AOP保持一致。 spring: datasource: names: user,soul user: jdbc-url: jdbc:mysql://127.0.0.1:3306/study_user?useSSL=false&useUnicode=true&characterEncoding=UTF-8 username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource #hikari连接池配置 hikari: #最小空闲连接数 minimum-idle: 5 #最大连接池 maximum-pool-size: 20 #链接超时时间 3秒 connection-timeout: 3000 soul: jdbc-url: jdbc:mysql://127.0.0.1:3306/soul?useSSL=false&useUnicode=true&characterEncoding=UTF-8 username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver type: com.zaxxer.hikari.HikariDataSource #hikari连接池配置 hikari: #最小空闲连接数 minimum-idle: 5 #最大连接池 maximum-pool-size: 20 #链接超时时间 3秒 connection-timeout: 3000 结束
  • [技术干货] 当注入的 Bean 存在冲突时,到底有多少种解决方案?
    当我们从 Spring 容器中“拉”取一个 Bean 回来的时候,可以按照名字去拉取,也可以按照类型去拉取,按照 BeanName 拉取的话,一般来说只要 BeanName 书写没有问题,都是没问题的。但是如果是按照类型去拉取,则可能会因为 Bean 存在多个实例从而导致失败。在前面的文章中,松哥和小伙伴们分享了 @Primary、@Qualifier 注解在处理该问题时的一些具体的方案,但是都是零散的,今天咱们来把这些方案总结一下,顺便再来看看是否还存在其他方案?1. 问题呈现假设我有 A、B 两个类,在 A 中注入 B,如下:@Component public class A {     @Autowired     B b; }至于 B,则在配置类中存在多个实例:@Configuration @ComponentScan public class JavaConfig {     @Bean("b1")     B b1() {         return new B();     }     @Bean("b2")     B b2() {         return new B();     } }这样的项目启动之后,必然会抛出如下异常:今天我们就来总结下这个问题的解决方案。2. 解决方案分析2.1 @Resource使用 @Resource 注解,这个应该是大家最容易想到的方案之一,不过使用 @Resource 注解需要额外添加依赖:<dependency>     <groupId>jakarta.annotation</groupId>     <artifactId>jakarta.annotation-api</artifactId>     <version>2.1.1</version> </dependency>加了依赖之后,现在就可以直接使用 @Resource 注解了:@Service public class A {     @Resource(name = "b1")     B b; }2.2 @Qualifier 指定 name另一种方案就是搭配 @Qualifier 注解,通过该注解指定 Bean 的名称:@Service public class A {     @Autowired     @Qualifier("b1")     B b; }关于这种方案的源码分析松哥在之前的文章中和大家聊过了:Spring 中 @Qualifier 注解还能这么用?。2.3 @Qualifier 不指定 name这种方案也是搭配 @Qualifier,但是并不指定 BeanName,而是在 B 注册和 A 中注入 B 的时候,分别标记一个 @Qualifier 注解:@Service public class A {     @Autowired     @Qualifier     B b; } @Configuration @ComponentScan public class JavaConfig {     @Bean     @Qualifier     B b1() {         return new B();     }     @Bean     B b2() {         return new B();     } }关于这种方案的源码分析松哥在之前的文章中和大家聊过了:Spring 中 @Qualifier 注解还能这么用?。2.4 不作为候选 Bean另外还有一种方案,就是在注册 Bean 的时候,告诉 Spring 容器,这个 Bean 在通过 type 进行注入的时候,不作为候选 Bean。小伙伴们知道,在第一小节中报的错,原因就是因为根据 type 去查找相应的 Bean 的时候,找到了多个候选 Bean,所以才会报错,所以我们注册一个 Bean 的时候,可以设置该 Bean 不是候选 Bean,这个设置并不影响通过 name 注入一个 Bean。具体配置如下:Java 代码配置:@Configuration @ComponentScan public class JavaConfig {     @Bean(autowireCandidate = false)     B b1() {         return new B();     }     @Bean     B b2() {         return new B();     } }autowireCandidate 属性就表示这个 Bean 不是一个候选 Bean。XML 配置:<bean class="org.javaboy.bean.p2.B" autowire-candidate="false"/>autowire-candidate 属性表示当前 Bean 是否作为一个候选 Bean。2.5 @Primary差点把我们最常用的方案忘了。@Primary 表示当通过 type 注入的时候,如果当前 Bean 存在多个实例,则优先使用带有 @Primary 注解的 Bean。@Service public class A {     @Autowired     B b; } @Configuration @ComponentScan public class JavaConfig {     @Bean     @Primary     B b1() {         return new B();     }     @Bean     B b2() {         return new B();     } }原文链接
  • [技术干货] Spring 中 Bean 的作用域有哪些?
    Spring 中的 Bean 可以有不同的作用域,这些作用域决定了 Bean 的生命周期和可用性。以下是 Spring 中常见的几种 Bean 作用域:Singleton(单例)作用域Singleton 作用域是 Spring 中默认的作用域,一个被定义为 Singleton 的 Bean 在整个应用程序中只有一个实例。这意味着无论在哪个地方获取该 Bean,都会得到同一个实例。Singleton 作用域适用于需要全局共享的组件,例如配置管理、日志记录等。RequestScope(请求作用域)RequestScope 作用域的 Bean 在每个 HTTP 请求中都是唯一的,当请求结束时,该 Bean 会被销毁。这种作用域通常用于处理与客户端会话相关的数据,例如购物车、用户信息等。需要注意的是,如果多个请求同时访问同一个 RequestScope 的 Bean,可能会出现并发问题。SessionScope(会话作用域)SessionScope 作用域的 Bean 在每个用户会话中都是唯一的,当用户会话结束时,该 Bean 会被销毁。这种作用域通常用于处理用户登录状态和用户相关信息。需要注意的是,如果多个用户同时访问同一个 SessionScope 的 Bean,可能会出现并发问题。ApplicationScope(应用作用域)ApplicationScope 作用域的 Bean 在每次应用程序启动时创建,并在应用程序关闭时销毁。这种作用域通常用于管理应用程序级别的资源和对象,例如缓存、线程池等。需要注意的是,由于 ApplicationScope 作用域的 Bean 是全局的,因此需要注意并发问题和资源泄漏问题。WebSocketScope(WebSocket作用域)WebSocketScope 作用域的 Bean 在每个 WebSocket 连接中都是唯一的,当连接关闭时,该 Bean