• [技术干货] 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
  • [技术干货] Spring 项目过程及如何使用 Spring-转载
     1.1 创建 Maven 项目  之后会进入一个修改项目名称的页面:  1.2添加 Spring 框架支持 Spring 框架支持添加到 pom.xml 这个文件当中,添加的内容我放在下面,这里不用记,不用记,不用记!!!需要的时候复制粘贴即可;在添加完毕后,可以刷新一下项目,加载一下依赖。  <dependencies>         <dependency>             <groupId>org.springframework</groupId>             <artifactId>spring-context</artifactId>             <version>5.2.3.RELEASE</version>         </dependency>         <dependency>             <groupId>org.springframework</groupId>             <artifactId>spring-beans</artifactId>             <version>5.2.3.RELEASE</version>         </dependency>     </dependencies> 1.3 添加启动项 这个没啥说的了,就是需要一个入口方法(main方法),自己看看放在哪里合适,就放置哪里就行了。  2.如何使用 Spring 我们知道 Spring 它是一个包含了众多个工具方法的IoC容器,容器的的最基本的应用就是,装东西和将装进去的东西取出来,Spring 也是这样:  将对象存储到容器(Spring)中 在容器中把对象取出来 对这里不了解的可以看这篇文章Spring 核心与设计思想 接下里对于 Spring 如何使用,我们就围绕着存取对象进行。  注:在 Java 语言中普通对象也叫做Bean  2.1 存储 Bean 对象 存储 Bean对象分为两步走:  先创建一个 Bean对象(存在才可以存嘛!) 将创建的Bean注册到Spring容器中 具体操作看向下看:  2.1.1 创建 Bean对象 上面说了Bean对象就是 Java 语言中的普通对象,这步就非常简单了!  package com;  public class User {     public String play() {         return "sing + jump + rap";     } } 2.1.2 将 Bean对象注册到容器中 在创建好的项⽬中添加 Spring 配置⽂件 spring-config.xml,将此⽂件放到 resources 的根⽬录下,这里这个配置文件名称可以是任意的名字,这里取这个名字是因为它符合开发标准,创建好的配置文件中需要初始化好一些配置,我将它放在下面了,这里也和上面的那个一样,不用去记,在使用的时候,把这部分复制进来就行了。  <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </beans> 配置好上面的内容之后就可以向里面添加 Bean对象了,这里需要注意一点,在添加class属性时,如果这个这个类在一包地下,需要将这个包的绝对路径打出来,代码如下:          <bean id="user" class="com.UserController"></bean> 1 把行代码加入到<beans></beans>这个组标签里就算注册完 Bean对象了,如下图:  2.2 获取并使用 Bean对象 2.2.1 使用 ApplicationContext 获取对象 获取并使用 Bean对象,分为三步:  得到 Spring 上下文对象,我们把对象交给 Spring 管理了,因此获取 对象是从 Spring 中获取,就必须先得到 Spring 的上下文。 通过 Spring 上下文,获取某个指定的 Bean对象 使用 Bean对象。 注:如果需要取多个对象,复用2、3步即可  具体的操作看下面的代码:  package com;  import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext;  public class App {     public static void main(String[] args) {         //1.加载配置(获取所以注册 bean信息)         ApplicationContext context =new ClassPathXmlApplicationContext("spring-config.xml");         //2.加载使用的bean对象         UserController user = context.getBean("user",UserController.class);         //3.调用需要的方法         System.out.println(user.play());     }  } 2.2.2 使用 BeanFactory 获取对象(了解即可) 在取 Bean对象时,还有另一种方式,使用ApplicationContext 的父类BeanFactory,这种方式现在已经被淘汰了,那它们连个有什么区别呢?  public static void main(String[] args) {         //1.加载配置(获取所以注册 bean信息)         BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("spring-config.xml"));         //2.加载使用的bean对象         UserController user = beanFactory.getBean("user",UserController.class);         //3.调用需要的方法         System.out.println(user.play());     } 3.拓展 ApplicationContext 与BeanFactory 的区别(常见面试题):  继承关系和功能⽅⾯来说:Spring 容器有两个顶级的接⼝:BeanFactory和ApplicationContext。其中 BeanFactory 提供了基础的访问容器的能⼒⽽ ApplicationContext属于 BeanFactory 的⼦类,它除了继承了 BeanFactory的所有功能之外,它还拥有独特的特性,还添加了对国际化⽀持、资源访⽀持、以及事件传播等⽅⾯的⽀持。  从性能⽅⾯来说:ApplicationContext 是⼀次性加载并初始化所有的 Bean对象,⽽BeanFactory 是需要那个才去加载那个,因此更加轻量。 ———————————————— 版权声明:本文为CSDN博主「爱吃大白菜  」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_65228171/article/details/131357803 
  • [其他] SpringMV工作原理和springboot自动配置原理
    1.SpringMV工作原理SpringMVC工作过程围绕着前端控制器DispatchServerlet,几个重要组件有HandleMapping(处理器映射器)、HandleAdapter(处理器适配器)、ViewReslover(试图解析器)工作流程:(1)DispatchServerlet接收用户请求将请求发送给HandleMapping(2)HandleMapping根据请求url找到具体的handle和拦截器,返回给DispatchServerlet(3)DispatchServerlet调用HandleAdapter,HandleAdapter执行具体的controller,并将controller返回的ModelAndView返回给DispatchServler(4)DispatchServerlet将ModelAndView传给ViewReslover,ViewReslover解析后返回具体view(5)DispatchServerlet根据view进行视图渲染,返回给用户2.springboot自动配置原理启动类@SpringbootApplication注解下,有三个关键注解(1)@springbootConfiguration:表示启动类是一个自动配置类(2)@CompontScan:扫描启动类所在包外的组件到容器中(3)@EnableConfigutarion:最关键的一个注解,他拥有两个子注解,其中@AutoConfigurationpackageu会将启动类所在包下的所有组件到容器中,@Import会导入一个自动配置文件选择器,他会去加载META_INF目录下的spring.factories文件,这个文件中存放很大自动配置类的全类名,这些类会根据元注解的装配条件生效,生效的类就会被实例化,加载到ioc容器中
  • [其他] spring中用了哪些设计模式
    Spring框架中常用的设计模式包括:工厂方法模式(Factory Method):【BeanFactory】工厂方法模式是一种创建型设计模式,它提供了一种创建对象的最佳方式。在工厂方法模式中,客户端不需要知道产品内部的具体实现细节,将产品的创建和具体实现 细节封装在一个工厂类中,客户端只需要通过工厂类来创建产品,具体产品的实现由工厂类负责。动态代理模式:【AOP】动态代理模式是一种行为型设计模式,它允许我们在运行时动态地创建代理对象,并将代理对象的行为委托给另一个对象。在Spring框架中,我们可以使用Java动态代理来实现这一功能。模板方法模式:【RestTemplate】模板方法模式是一种创建型设计模式,它提供了一种设计模式,使得可以将算法的骨架与算法的执行分离。在模板方法模式中,我们定义一个算法骨架,然后针对不同的具体情况,使用不同的实现来构造算法的执行。在Spring框架中,我们可以使用模板方法模式来创建对象的算法骨架,然后针对不同的具体情况,使用不同的实现来构造算法的执行。例如,我们可以定义一个算法骨架,然后针对不同的租户,使用不同的具体实现来构造具体的服务。适配器模式:【SpringMVC中handlerAdaper】适配器模式是一种行为型设计模式,它可以将一个类的接口转换为另一个类的接口,使得两个类可以进行相互通信。在Spring框架中,我们可以使用适配器模式来实现不同类之间的转换,将一个对象转换为另一个对象,或者将一个类的接口转换为另一个类的接口。在Spring中,常用的适配器模式实现类是Hessian2Asi适配器和JSON适配器。Hessian2Asi适配器可以将Hessian2序列化的对象转换为Java对象,而JSON适配器可以将JSON字符串转换为Java对象。观察者模式:【Spring里的监听器】观察者模式是一种行为型设计模式,它定义了一种依赖模式,其中一个对象(称为主题)维护了一系列依赖于它的观察者对象,当主题的状态发生变化时,它会通知它的所有观察者对象,使得它们可以自动更新。在Spring框架中,我们可以使用观察者模式来实现事件的通知和处理。在Spring中,常用的观察者模式实现类是PubSub模型和Event模型。PubSub模型使用观察者模式来实现消息的发布和订阅,而Event模型则使用观察者模式来实现事件的发布和订阅。
  • [技术干货] Spring 中对 controller和service层的一些见解
    接触Spring Boot开发一年不到,回想起前几年使用spring MVC的时候,因为当时公司业务比较简单,所以service层和dao层实际上是一样的,业务逻辑全部放在了controller层来做;当时觉得很纳闷,service层感觉是多余的,根本用不到;最近接触的项目,架构师设计的框架,直接根据模型设计dao层接口和service接口,代码写了不少,突然发现这么定义接口很多功能是没法实现的。于是回头重新思考了spring MVC模型,刚才看了篇 非常不错的博客 ,感觉作者能把这个问题解释清楚了。还是从MVC三层模型开始,这三层模型的设计之初,就是为了将业务层(controller)、视图层(view)以及模型层(modal)区分开来。需要注意的是,这里并没有数据库这个概念,所以模型层会有一些冗杂,两个表的联合查询出来的数据,会被封装成一个模型交给控制层;同样的,控制层因为没有服务的概念,如果项目比较大,也会变的有些冗余。基于controller和modal层并没有很好的实现模块化,因此,我们将modal层去掉,改为更加原子化的dao层;同时,将controller层的业务逻辑,划分成多个服务(service)。每个服务可以组合使用dao层数据,组装成一个服务,比如用户的注册服务;而controller层,调用多个service服务完成url请求。简单来说,增加service层,替换modal层,第一是细化了数据模型,使得我们在改动某张表时,只需要改动dao层实现即可,最大化的减少了代码的改动成本;当然,更多的情况是service服务和controller可能都需要更改; service层将controller的逻辑分类,保证了controller的逻辑更加清晰。举个生活中的例子,用户预约某个酒店的客房,这是酒店首先会调用验证服务对用户提供的信息进行验证,之后调用预约服务进行预约,如果预约失败,酒店可能会把客户的预约信息提交给另外一家酒店请求它们的预约服务,然后将结果返回给客户;对于服务层来说,需要判断酒店是否有空余客房,之后修改客房信息,同时将客房和用户信息存入临时表。这里至少需要两种不同的dao层服务实现service。所以整体上来看,controllrt->service->dao至少是一对一,更多的情况下是一对多。这也就是service层存在的意义了。
  • [技术干货] 新版SpringSecurity如何自定义JSON登录
    目前最新版的 Spring Boot 已经到了 3.0.5 了,随之而来 Spring Security 目前的版本也到了 6.0.2 了,最近几次的版本升级,Spring Security 写法的变化特别多。最近有小伙伴在  Spring Security 中自定义 JSON 登录的时候就遇到问题了,我看了下,感觉这个问题还特别典型,因此我拎出来和各位小伙伴一起来聊一聊这个话题。一. 自定义 JSON 登录小伙伴们知道,Spring Security 中默认的登录接口数据格式是 key-value 的形式,如果我们想使用 JSON 格式来登录,那么就必须自定义过滤器或者自定义登录接口,下面松哥先来和小伙伴们展示一下这两种不同的登录形式。1.1 自定义登录过滤器Spring Security 默认处理登录数据的过滤器是 UsernamePasswordAuthenticationFilter,在这个过滤器中,系统会通过 request.getParameter(this.passwordParameter) 的方式将用户名和密码读取出来,很明显这就要求前端传递参数的形式是 key-value。如果想要使用 JSON 格式的参数登录,那么就需要从这个地方做文章了,我们自定义的过滤器如下: codeduidaima.compublic class JsonLoginFilter extends UsernamePasswordAuthenticationFilter {@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {//堆代码 duidaima.com//获取请求头,据此判断请求参数类型String contentType = request.getContentType();if (MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(contentType) || MediaType.APPLICATION_JSON_UTF8_VALUE.equalsIgnoreCase(contentType)) {//说明请求参数是 JSONif (!request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}String username = null;String password = null;try {//解析请求体中的 JSON 参数User user = new ObjectMapper().readValue(request.getInputStream(), User.class);username = user.getUsername();username = (username != null) ? username.trim() : "";password = user.getPassword();password = (password != null) ? password : "";} catch (IOException e) {throw new RuntimeException(e);}//构建登录令牌UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,password);// Allow subclasses to set the "details" propertysetDetails(request, authRequest);//执行真正的登录操作Authentication auth = this.getAuthenticationManager().authenticate(authRequest);return auth;} else {return super.attemptAuthentication(request, response);}}}首先我们获取请求头,根据请求头的类型来判断请求参数的格式。如果是 JSON 格式的参数,就在 if 中进行处理,否则说明是 key-value 形式的参数,那么我们就调用父类的方法进行处理即可。JSON 格式的参数的处理逻辑和 key-value 的处理逻辑是一致的,唯一不同的是参数的提取方式不同而已。最后,我们还需要对这个过滤器进行配置: codeduidaima.com@Configurationpublic class SecurityConfig {@AutowiredUserService userService;@BeanJsonLoginFilter jsonLoginFilter() {JsonLoginFilter filter = new JsonLoginFilter();filter.setAuthenticationSuccessHandler((req,resp,auth)->{resp.setContentType("application/json;charset=utf-8");PrintWriter out = resp.getWriter();//获取当前登录成功的用户对象User user = (User) auth.getPrincipal();user.setPassword(null);RespBean respBean = RespBean.ok("登录成功", user);out.write(new ObjectMapper().writeValueAsString(respBean));});filter.setAuthenticationFailureHandler((req,resp,e)->{resp.setContentType("application/json;charset=utf-8");PrintWriter out = resp.getWriter();RespBean respBean = RespBean.error("登录失败");if (e instanceof BadCredentialsException) {respBean.setMessage("用户名或者密码输入错误,登录失败");} else if (e instanceof DisabledException) {respBean.setMessage("账户被禁用,登录失败");} else if (e instanceof CredentialsExpiredException) {respBean.setMessage("密码过期,登录失败");} else if (e instanceof AccountExpiredException) {respBean.setMessage("账户过期,登录失败");} else if (e instanceof LockedException) {respBean.setMessage("账户被锁定,登录失败");}out.write(new ObjectMapper().writeValueAsString(respBean));});filter.setAuthenticationManager(authenticationManager());filter.setFilterProcessesUrl("/login");return filter;}@BeanAuthenticationManager authenticationManager() {DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();daoAuthenticationProvider.setUserDetailsService(userService);ProviderManager pm = new ProviderManager(daoAuthenticationProvider);return pm;}@BeanSecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {//开启过滤器的配置http.authorizeHttpRequests()//任意请求,都要认证之后才能访问.anyRequest().authenticated().and()//开启表单登录,开启之后,就会自动配置登录页面、登录接口等信息.formLogin()//和登录相关的 URL 地址都放行.permitAll().and()//关闭 csrf 保护机制,本质上就是从 Spring Security 过滤器链中移除了 CsrfFilter.csrf().disable();http.addFilterBefore(jsonLoginFilter(), UsernamePasswordAuthenticationFilter.class);return http.build();}}这里就是配置一个 JsonLoginFilter 的 Bean,并将之添加到 Spring Security 过滤器链中即可。在 Spring Boot3 之前(Spring Security6 之前),上面这段代码就可以实现 JSON 登录了。但是从 Spring Boot3 开始,这段代码有点瑕疵了,直接用已经无法实现 JSON 登录了,具体原因我下文分析。1.2 自定义登录接口另外一种自定义 JSON 登录的方式是直接自定义登录接口,如下: codeduidaima.com@RestControllerpublic class LoginController {@AutowiredAuthenticationManager authenticationManager;@PostMapping("/doLogin")public String doLogin(@RequestBody User user) {UsernamePasswordAuthenticationToken unauthenticated = UsernamePasswordAuthenticationToken.unauthenticated(user.getUsername(), user.getPassword());try {Authentication authenticate = authenticationManager.authenticate(unauthenticated);SecurityContextHolder.getContext().setAuthentication(authenticate);return "success";} catch (AuthenticationException e) {return "error:" + e.getMessage();}}}这里直接自定义登录接口,请求参数通过 JSON 的形式来传递。拿到用户名密码之后,调用 AuthenticationManager#authenticate 方法进行认证即可。认证成功之后,将认证后的用户信息存入到 SecurityContextHolder 中。最后再配一下登录接口就行了: codeduidaima.com@Configurationpublic class SecurityConfig {@AutowiredUserService userService;@BeanAuthenticationManager authenticationManager() {DaoAuthenticationProvider provider = new DaoAuthenticationProvider();provider.setUserDetailsService(userService);ProviderManager pm = new ProviderManager(provider);return pm;}@BeanSecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests()//表示 /doLogin 这个地址可以不用登录直接访问.requestMatchers("/doLogin").permitAll().anyRequest().authenticated().and().formLogin().permitAll().and().csrf().disable();return http.build();}}这也算是一种使用 JSON 格式参数的方案。在 Spring Boot3 之前(Spring Security6 之前),上面这个方案也是没有任何问题的。从 Spring Boot3(Spring Security6) 开始,上面这两种方案都出现了一些瑕疵。具体表现就是:当你调用登录接口登录成功之后,再去访问系统中的其他页面,又会跳转回登录页面,说明访问登录之外的其他接口时,系统不知道你已经登录过了。二. 原因分析产生上面问题的原因,主要在于 Spring Security 过滤器链中有一个过滤器发生变化了:在 Spring Boot3 之前,Spring Security 过滤器链中有一个名为 SecurityContextPersistenceFilter 的过滤器,这个过滤器在 Spring Boot2.7.x 中废弃了,但是还在使用,在 Spring Boot3 中则被从 Spring Security 过滤器链中移除了,取而代之的是一个名为 SecurityContextHolderFilter 的过滤器。在第一小节和小伙伴们介绍的两种 JSON 登录方案在 Spring Boot2.x 中可以运行在 Spring Boot3.x 中无法运行,就是因为这个过滤器的变化导致的。所以接下来我们就来分析一下这两个过滤器到底有哪些区别。先来看 SecurityContextPersistenceFilter 的核心逻辑: codeduidaima.comprivate void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws IOException, ServletException {HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);try {SecurityContextHolder.setContext(contextBeforeChainExecution);chain.doFilter(holder.getRequest(), holder.getResponse());}finally {SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();SecurityContextHolder.clearContext();this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());}}我这里只贴出来了一些关键的核心代码:首先,这个过滤器位于整个 Spring Security 过滤器链的第三个,是非常靠前的。当登录请求经过这个过滤器的时候,首先会尝试从 SecurityContextRepository(上文中的 this.repo)中读取到 SecurityContext 对象,这个对象中保存了当前用户的信息,第一次登录的时候,这里实际上读取不到任何用户信息。将读取到的 SecurityContext 存入到 SecurityContextHolder 中,默认情况下,SecurityContextHolder 中通过 ThreadLocal 来保存 SecurityContext 对象,也就是当前请求在后续的处理流程中,只要在同一个线程里,都可以直接从 SecurityContextHolder 中提取到当前登录用户信息。请求继续向后执行。在 finally 代码块中,当前请求已经结束了,此时再次获取到 SecurityContext,并清空 SecurityContextHolder 防止内存泄漏,然后调用 this.repo.saveContext 方法保存当前登录用户对象(实际上是保存到 HttpSession 中)。以后其他请求到达的时候,执行前面第 2 步的时候,就读取到当前用户的信息了,在请求后续的处理过程中,Spring Security 需要知道当前用户的时候,会自动去 SecurityContextHolder 中读取当前用户信息。这就是 Spring Security 认证的一个大致流程。然而,到了 Spring Boot3 之后,这个过滤器被 SecurityContextHolderFilter 取代了,我们来看下 SecurityContextHolderFilter 过滤器的一个关键逻辑: codeduidaima.comprivate void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException {Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);try {this.securityContextHolderStrategy.setDeferredContext(deferredContext);chain.doFilter(request, response);}finally {this.securityContextHolderStrategy.clearContext();request.removeAttribute(FILTER_APPLIED);}}小伙伴们看到,前面的逻辑基本上还是一样的,不一样的是 finally 中的代码,finally 中少了一步向 HttpSession 保存 SecurityContext 的操作。这下就明白了,用户登录成功之后,用户信息没有保存到 HttpSession,导致下一次请求到达的时候,无法从 HttpSession 中读取到 SecurityContext 存到 SecurityContextHolder 中,在后续的执行过程中,Spring Security 就会认为当前用户没有登录。这就是问题的原因!找到原因,那么问题就好解决了。三. 问题解决首先问题出在了过滤器上,直接改过滤器倒也不是不可以,但是,既然 Spring Security 在升级的过程中抛弃了之前旧的方案,我们又费劲的把之前旧的方案写回来,好像也不合理。其实,Spring Security 提供了另外一个修改的入口,在 org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#successfulAuthentication 方法中,源码如下: codeduidaima.comprotected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication authResult) throws IOException, ServletException {SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();context.setAuthentication(authResult);this.securityContextHolderStrategy.setContext(context);this.securityContextRepository.saveContext(context, request, response);this.rememberMeServices.loginSuccess(request, response, authResult);if (this.eventPublisher != null) {this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));}this.successHandler.onAuthenticationSuccess(request, response, authResult);}这个方法是当前用户登录成功之后的回调方法,小伙伴们看到,在这个回调方法中,有一句 this.securityContextRepository.saveContext(context, request, response);,这就表示将当前登录成功的用户信息存入到 HttpSession 中。在当前过滤器中,securityContextRepository 的类型是 RequestAttributeSecurityContextRepository,这个表示将 SecurityContext 存入到当前请求的属性中,那很明显,在当前请求结束之后,这个数据就没了。在 Spring Security 的自动化配置类中,将 securityContextRepository 属性指向了 DelegatingSecurityContextRepository,这是一个代理的存储器,代理的对象是 RequestAttributeSecurityContextRepository 和 HttpSessionSecurityContextRepository,所以在默认的情况下,用户登录成功之后,在这里就把登录用户数据存入到 HttpSessionSecurityContextRepository 中了。当我们自定义了登录过滤器之后,就破坏了自动化配置里的方案了,这里使用的 securityContextRepository 对象就真的是 RequestAttributeSecurityContextRepository 了,所以就导致用户后续访问时系统以为用户未登录。那么解决方案很简单,我们只需要为自定义的过滤器指定 securityContextRepository 属性的值就可以了,如下: codeduidaima.com// 堆代码 duidaima.com@BeanJsonLoginFilter jsonLoginFilter() {JsonLoginFilter filter = new JsonLoginFilter();filter.setAuthenticationSuccessHandler((req,resp,auth)->{resp.setContentType("application/json;charset=utf-8");PrintWriter out = resp.getWriter();//获取当前登录成功的用户对象User user = (User) auth.getPrincipal();user.setPassword(null);RespBean respBean = RespBean.ok("登录成功", user);out.write(new ObjectMapper().writeValueAsString(respBean));});filter.setAuthenticationFailureHandler((req,resp,e)->{resp.setContentType("application/json;charset=utf-8");PrintWriter out = resp.getWriter();RespBean respBean = RespBean.error("登录失败");if (e instanceof BadCredentialsException) {respBean.setMessage("用户名或者密码输入错误,登录失败");} else if (e instanceof DisabledException) {respBean.setMessage("账户被禁用,登录失败");} else if (e instanceof CredentialsExpiredException) {respBean.setMessage("密码过期,登录失败");} else if (e instanceof AccountExpiredException) {respBean.setMessage("账户过期,登录失败");} else if (e instanceof LockedException) {respBean.setMessage("账户被锁定,登录失败");}out.write(new ObjectMapper().writeValueAsString(respBean));});filter.setAuthenticationManager(authenticationManager());filter.setFilterProcessesUrl("/login");filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());return filter;}小伙伴们看到,最后调用 setSecurityContextRepository 方法设置一下就行。Spring Boot3.x 之前之所以不用设置这个属性,是因为这里虽然没保存最后还是在 SecurityContextPersistenceFilter 过滤器中保存了。那么对于自定义登录接口的问题,解决思路也是类似的: codeduidaima.com@RestControllerpublic class LoginController {@AutowiredAuthenticationManager authenticationManager;@PostMapping("/doLogin")public String doLogin(@RequestBody User user, HttpSession session) {UsernamePasswordAuthenticationToken unauthenticated = UsernamePasswordAuthenticationToken.unauthenticated(user.getUsername(), user.getPassword());try {Authentication authenticate = authenticationManager.authenticate(unauthenticated);SecurityContextHolder.getContext().setAuthentication(authenticate);session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());return "success";} catch (AuthenticationException e) {return "error:" + e.getMessage();}}}小伙伴们看到,在登录成功之后,开发者自己手动将数据存入到 HttpSession 中,这样就能确保下个请求到达的时候,能够从 HttpSession 中读取到有效的数据存入到 SecurityContextHolder 中了。好啦,Spring Boot 新旧版本交替中,一个小小的问题,希望小伙伴们能够有所收获转载自cid:link_0Group/Topic/JAVA/10494
  • SpringBoot结合XXL-JOB实现定时任务-转载
    ​  前言 上篇文章我们介绍了 Quartz 的使用,当时实现了两个简单的需求,不过最后我们总结的时候也提到 Quartz 有不少缺点,代码侵入太严重,所以本篇将介绍 xxl-job 这个定时任务框架。   Quartz的不足 Quartz 的不足:Quartz 作为开源任务调度中的佼佼者,是任务调度的首选。但是在集群环境中,Quartz采用API的方式对任务进行管理,这样存在以下问题:   通过调用API的方式操作任务,不人性化。 需要持久化业务的 QuartzJobBean 到底层数据表中,系统侵入性相当严重。 调度逻辑和QuartzJobBean耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务。 Xxl-job介绍 官方说明:XXL-JOB 是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。   通俗来讲:XXL-JOB 是一个任务调度框架,通过引入 XXL-JOB 相关的依赖,按照相关格式撰写代码后,可在其可视化界面进行任务的启动,执行,中止以及包含了日志记录与查询和任务状态监控。   更多详细介绍推荐阅读官方文档。   项目实践 Spring Boot集成XXL-JOB Spring Boot 集成 XXL-JOB 主要分为以下两步:   配置运行调度中心(xxl-job-admin) 配置运行执行器项目 xxl-job-admin 可以从源码仓库中下载代码,代码地址有两个:   GitHub:github.com/xuxueli/xxl… Gitee:gitee.com/xuxueli0323…  ​ 下载完之后,在 doc/db 目录下有数据库脚本 tables_xxl_job.sql,执行下脚本初始化调度数据库 xxl_job,如下图所示:  ​ 配置调度中心 将下载的源码解压,用 IDEA 打开,我们需要修改一下 xxl-job-admin 中的一些配置。(我这里下载的是最新版 2.3.1)   1、修改 application.properties,主要是配置一下 datasource 以及 email,其他不需要改变。   ### xxl-job, datasource spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver   ### xxl-job, email spring.mail.host=smtp.qq.com spring.mail.port=25 spring.mail.username=1739468244@qq.com spring.mail.from=1739468244@qq.com # 此处不是邮箱登录密码,而是开启SMTP服务后的授权码 spring.mail.password=xxxxx 2、修改 logback.xml,配置日志输出路径,我是在解压的 xxl-job-2.3.1 项目包中新建了一个 logs 文件夹。   <property name="log.path" value="/Users/xxx/xxl-job-2.3.1/logs/xxl-job-admin.log"/> 1 然后启动项目,正常启动后,访问地址为:http://localhost:8080/xxl-job-admin,默认的账户为 admin,密码为 123456,访问后台管理系统后台。   这样就表示调度中心已经搞定了,下一步就是创建执行器项目。   创建执行器项目 本项目与 Quartz 项目用的业务表和业务逻辑都一样,所以引入的依赖会比较多。   环境配置 1、引入依赖:   <parent>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-parent</artifactId>   <version>2.6.3</version>   <relativePath/> </parent>   <properties>   <java.version>1.8</java.version>   <fastjson.version>1.2.73</fastjson.version>   <hutool.version>5.5.1</hutool.version>   <mysql.version>8.0.19</mysql.version>   <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>   <org.projectlombok.version>1.18.20</org.projectlombok.version>   <druid.version>1.1.18</druid.version>   <springdoc.version>1.6.9</springdoc.version> </properties>   <dependencies>   <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-web</artifactId>   </dependency>     <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-aop</artifactId>   </dependency>   <dependency>     <groupId>com.xuxueli</groupId>     <artifactId>xxl-job-core</artifactId>     <version>2.3.1</version>   </dependency>     <dependency>     <groupId>com.baomidou</groupId>     <artifactId>mybatis-plus-boot-starter</artifactId>     <version>3.5.1</version>   </dependency>   <dependency>     <groupId>com.baomidou</groupId>     <artifactId>mybatis-plus</artifactId>     <version>3.5.1</version>   </dependency>   <dependency>     <groupId>mysql</groupId>     <artifactId>mysql-connector-java</artifactId>     <version>${mysql.version}</version>     <scope>runtime</scope>   </dependency>   <dependency>     <groupId>com.alibaba</groupId>     <artifactId>druid-spring-boot-starter</artifactId>     <version>${druid.version}</version>   </dependency>     <dependency>     <groupId>org.projectlombok</groupId>     <artifactId>lombok</artifactId>     <version>1.18.20</version>   </dependency>   <dependency>     <groupId>com.alibaba.fastjson2</groupId>     <artifactId>fastjson2</artifactId>     <version>2.0.12</version>   </dependency>   <dependency>     <groupId>org.mapstruct</groupId>     <artifactId>mapstruct</artifactId>     <version>${org.mapstruct.version}</version>   </dependency>   <dependency>     <groupId>org.mapstruct</groupId>     <artifactId>mapstruct-processor</artifactId>     <version>${org.mapstruct.version}</version>   </dependency>   <dependency>     <groupId>cn.hutool</groupId>     <artifactId>hutool-all</artifactId>     <version>${hutool.version}</version>   </dependency>   <dependency>     <groupId>org.springdoc</groupId>     <artifactId>springdoc-openapi-ui</artifactId>     <version>${springdoc.version}</version>   </dependency> </dependencies>   <build>   <plugins>     <plugin>       <groupId>org.springframework.boot</groupId>       <artifactId>spring-boot-maven-plugin</artifactId>     </plugin>   </plugins> </build> 2、application.yml 配置文件   server:   port: 9090   # xxl-job xxl:   job:     admin:       addresses: http://127.0.0.1:8080/xxl-job-admin # 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;     executor:       appname: hresh-job-executor # 执行器 AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册       ip: # 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";       port: 6666 # ### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;       logpath: /Users/xxx/xxl-job-2.3.1/logs/xxl-job # 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;       logretentiondays: 30 # 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;     accessToken: default_token  # 执行器通讯TOKEN [选填]:非空时启用;   spring:   application:     name: xxl-job-practice   datasource:     type: com.alibaba.druid.pool.DruidDataSource     driver-class-name: com.mysql.cj.jdbc.Driver     url: jdbc:mysql://localhost:3306/xxl_job?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false     username: root     password: root   mybatis:   mapper-locations: classpath:mapper/*Mapper.xml   configuration:     log-impl: org.apache.ibatis.logging.stdout.StdOutImpl     lazy-loading-enabled: true 上述 xxl-job 的 logpath 配置与调度中心的输出日志用的是同一个目录,accessToken 也与调度中心的 xxl.job.accessToken 一致。   核心类 1、xxl-job 配置类   @Configuration public class XxlJobConfig {     @Value("${xxl.job.admin.addresses}")   private String adminAddresses;   @Value("${xxl.job.executor.appname}")   private String appName;   @Value("${xxl.job.executor.ip}")   private String ip;   @Value("${xxl.job.executor.port}")   private int port;   @Value("${xxl.job.accessToken}")   private String accessToken;   @Value("${xxl.job.executor.logpath}")   private String logPath;   @Value("${xxl.job.executor.logretentiondays}")   private int logRetentionDays;     @Bean   public XxlJobSpringExecutor xxlJobExecutor() {     // 创建 XxlJobSpringExecutor 执行器     XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();     xxlJobSpringExecutor.setAdminAddresses(adminAddresses);     xxlJobSpringExecutor.setAppname(appName);     xxlJobSpringExecutor.setIp(ip);     xxlJobSpringExecutor.setPort(port);     xxlJobSpringExecutor.setAccessToken(accessToken);     xxlJobSpringExecutor.setLogPath(logPath);     xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);     // 返回     return xxlJobSpringExecutor;   } } 2、xxl-job 工具类   @Component @RequiredArgsConstructor public class XxlUtil {     @Value("${xxl.job.admin.addresses}")   private String xxlJobAdminAddress;     private final RestTemplate restTemplate;     // 请求Url   private static final String ADD_INFO_URL = "/jobinfo/addJob";   private static final String REMOVE_INFO_URL = "/jobinfo/removeJob";   private static final String GET_GROUP_ID = "/jobgroup/loadByAppName";     /**    * 添加任务    *    * @param xxlJobInfo    * @param appName    * @return    */   public String addJob(XxlJobInfo xxlJobInfo, String appName) {     Map<String, Object> params = new HashMap<>();     params.put("appName", appName);     String json = JSONUtil.toJsonStr(params);     String result = doPost(xxlJobAdminAddress + GET_GROUP_ID, json);     JSONObject jsonObject = JSON.parseObject(result);     Map<String, Object> map = (Map<String, Object>) jsonObject.get("content");     Integer groupId = (Integer) map.get("id");     xxlJobInfo.setJobGroup(groupId);     String xxlJobInfoJson = JSONUtil.toJsonStr(xxlJobInfo);     return doPost(xxlJobAdminAddress + ADD_INFO_URL, xxlJobInfoJson);   }     // 删除job   public String removeJob(long jobId) {     MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();     map.add("id", String.valueOf(jobId));     return doPostWithFormData(xxlJobAdminAddress + REMOVE_INFO_URL, map);   }     /**    * 远程调用    *    * @param url    * @param json    */   private String doPost(String url, String json) {     HttpHeaders headers = new HttpHeaders();     headers.setContentType(MediaType.APPLICATION_JSON);     HttpEntity<String> entity = new HttpEntity<>(json, headers);     ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, entity, String.class);     return responseEntity.getBody();   }     private String doPostWithFormData(String url, MultiValueMap<String, String> map) {     HttpHeaders headers = new HttpHeaders();     headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);     HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers);     ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, entity, String.class);     return responseEntity.getBody();   } } 此处我们利用 RestTemplate 来远程调用 xxl-job-admin 中的服务,从而实现动态创建定时任务,而不是局限于通过 UI 界面来创建任务。   这里我们用到三个接口,都需要我们在 xxl-job-admin 中手动添加,这样在调用接口时,就不需要登录验证了,这就要求在定义接口时加上一个 PermissionLimit并设置 limit 为 false,那么这样就不用去登录就可以调用接口。   3、修改 JobGroupController,新增 loadByAppName 方法   @RequestMapping("/loadByAppName") @ResponseBody @PermissionLimit(limit = false) public ReturnT<XxlJobGroup> loadByAppName(@RequestBody Map<String, Object> map) {   XxlJobGroup jobGroup = xxlJobGroupDao.loadByAppName(map);   return jobGroup != null ? new ReturnT<XxlJobGroup>(jobGroup)     : new ReturnT<XxlJobGroup>(ReturnT.FAIL_CODE, null); } XxlJobGroupDao 文件以及对应的 xml 文件   XxlJobGroup loadByAppName(Map<String, Object> map); 1 <select id="loadByAppName" parameterType="java.util.HashMap" resultMap="XxlJobGroup">         SELECT         <include refid="Base_Column_List"/>         FROM xxl_job_group AS t         WHERE t.app_name = #{appName}     </select> 4、修改 JobInfoController,增加 addJob 方法和 removeJob 方法   @RequestMapping("/addJob")     @ResponseBody     @PermissionLimit(limit = false)     public ReturnT<String> addJob(@RequestBody XxlJobInfo jobInfo) {         return xxlJobService.add(jobInfo);     }       @RequestMapping("/removeJob")     @ResponseBody     @PermissionLimit(limit = false)     public ReturnT<String> removeJob(String id) {         return xxlJobService.remove(Integer.parseInt(id));     } addJob 方法与 JobInfoController 文件中的 add 方法具体逻辑是一样的,只是换个接口名。   @RequestMapping("/add")     @ResponseBody     public ReturnT<String> add(XxlJobInfo jobInfo) {         return xxlJobService.add(jobInfo);     } 至此,关于调度中心的修改就结束了。   5、XxlService 创建任务   @Service @Slf4j @RequiredArgsConstructor public class XxlService {     private final XxlUtil xxlUtil;     @Value("${xxl.job.executor.appname}")   private String appName;     public void addJob(XxlJobInfo xxlJobInfo) {     xxlUtil.addJob(xxlJobInfo, appName);     long triggerNextTime = xxlJobInfo.getTriggerNextTime();     log.info("任务已添加,将在{}开始执行任务", DateUtils.formatDate(triggerNextTime));   }   } 业务代码 1、UserService,包括用户注册,给用户发送欢迎消息,以及发送天气温度通知。   @Service @RequiredArgsConstructor @Slf4j public class UserService {     private final UserMapper userMapper;   private final UserStruct userStruct;   private final WeatherService weatherService;   private final XxlService xxlService;     /**    * 假设有这样一个业务需求,每当有新用户注册,则1分钟后会给用户发送欢迎通知.    *    * @param userRequest 用户请求体    */   @Transactional   public void register(UserRequest userRequest) {     if (Objects.isNull(userRequest) || isBlank(userRequest.getUsername()) ||         isBlank(userRequest.getPassword())) {       BusinessException.fail("账号或密码为空!");     }       User user = userStruct.toUser(userRequest);     userMapper.insert(user);       LocalDateTime scheduleTime = LocalDateTime.now().plusMinutes(1L);       XxlJobInfo xxlJobInfo = XxlJobInfo.builder().jobDesc("定时给用户发送通知").author("hresh")         .scheduleType("CRON").scheduleConf(DateUtils.getCron(scheduleTime)).glueType("BEAN")         .glueType("BEAN")         .executorHandler("sayHelloHandler")         .executorParam(user.getUsername())         .misfireStrategy("DO_NOTHING")         .executorRouteStrategy("FIRST")         .triggerNextTime(DateUtils.toEpochMilli(scheduleTime))         .executorBlockStrategy("SERIAL_EXECUTION").triggerStatus(1).build();       xxlService.addJob(xxlJobInfo);   }     public void sayHelloToUser(String username) {     if (StrUtil.isBlank(username)) {       log.error("用户名为空");     }     User user = userMapper.selectByUserName(username);     String message = "Welcome to Java,I am hresh.";     log.info(user.getUsername() + " , hello, " + message);   }     public void pushWeatherNotification() {     List<User> users = userMapper.queryAll();     log.info("执行发送天气通知给用户的任务。。。");     WeatherInfo weatherInfo = weatherService.getWeather(WeatherConstant.WU_HAN);     for (User user : users) {       log.info(user.getUsername() + "----" + weatherInfo.toString());     }   } } 2、WeatherService,获取天气温度等信息,这里就不贴代码了。   3、UserController,只有一个用户注册方法   @RestController @RequiredArgsConstructor public class UserController {     private final UserService userService;     @PostMapping("/register")   public Result<Object> register(@RequestBody UserRequest userRequest) {     userService.register(userRequest);     return Result.ok();   }   } 任务处理器 这里演示两种任务处理器,一种是用于处理 UI 页面创建的任务,另一种是处理代码创建的任务。   1、Dexxxxndler,仅用作演示,没什么实际含义。   @RequiredArgsConstructor @Slf4j public class Dexxxxndler extends IJobHandler {     @XxlJob(value = "dexxxxndler")   @Override   public void execute() throws Exception {     log.info("自动任务" + this.getClass().getSimpleName() + "执行");   } } 2、SayHelloHandler,用户注册后再 xxl-job 上创建一个任务,到时间后就调用该处理器。   @Component @RequiredArgsConstructor public class SayHelloHandler {     private final UserService userService;     @XxlJob(value = "sayHelloHandler")   public void execute() {     String param = XxlJobHelper.getJobParam();     userService.sayHelloToUser(param);   } } 在最新版本的 xxl-job 中,任务核心类 “IJobHandler” 的 “execute” 方法取消出入参设计。改为通过 “XxlJobHelper.getJobParam” 获取任务参数并替代方法入参,通过 “XxlJobHelper.handleSuccess/handleFail” 设置任务结果并替代方法出参,示例代码如下   @XxlJob("demoJobHandler") public void execute() {   String param = XxlJobHelper.getJobParam();    // 获取参数   XxlJobHelper.handleSuccess();                 // 设置任务结果 } 3、WeatherNotificationHandler,每天定时发送天气通知   @Component @RequiredArgsConstructor public class WeatherNotificationHandler extends IJobHandler {     private final UserService userService;     @XxlJob(value = "weatherNotificationHandler")   @Override   public void execute() throws Exception {     userService.pushWeatherNotification();   } } 测试 1、首先在执行器管理页面,点击新增按钮,弹出新增框。输入AppName (与application.yml中配置的appname保持一致),名称,注册方式默认自动注册,点击保存。 ​ 2、新增任务 ​ 控制台输出:   com.msdn.time.handler.Dexxxxndler        : 自动任务Dexxxxndler执行 1 2、利用 postman 来注册用户  ​ 去 UI 任务管理页面,可以看到代码创建的任务。  ​ 1分钟后,控制台输出如下:  ​ 3、在 UI 任务管理页面手动新增任务,用来发送天气通知。  ​ 点击执行一次,控制台输出如下:  ​ 实际应用中,对于手动创建的任务,直接点击启动就可以了。   这里还有一个问题,如果每次有新用户注册,都会创建一个定时任务,而且只执行一次,那么任务列表到时候就会有很多脏数据,所以我们在执行完发送欢迎通知后,就要删除。所以我们需要修改一下 SayHelloHandler   @XxlJob(value = "sayHelloHandler")   public void execute() {     String param = XxlJobHelper.getJobParam();     userService.sayHelloToUser(param);       long jobId = XxlJobHelper.getJobId();     xxlUtil.removeJob(jobId);   } 重启项目后,比如说明再创建一个名为 hresh2 的用户,然后任务列表就会新增一个任务。  ​ 等控制台输出 sayHello 后,可以发现任务列表中任务 ID 为 20的记录被删除掉了。   问题 控制台输出邮件注册错误 11:01:48.740 logback [RMI TCP Connection(1)-127.0.0.1] WARN  o.s.b.a.mail.MailHealthIndicator - Mail health check failed javax.mail.AuthenticationFailedException: 535 Login Fail. Please enter your authorization code to login. More information in http://service.mail.qq.com/cgi-bin/help?subtype=1&&id=28&&no=1001256 1 2 原因:xxl-job-admin 项目的 application.properties 文件中关于 spring.mail.password 的配置不对,可能有人配置了自己邮箱的登录密码。   解决方案: ​ ​ 总结 通过对比 Quartz 和 XXL-JOB 的使用,可以发现后者更易上手,代码侵入不严重,且具备可视化界面。这就是推荐新手使用 XXL-JOB 的原因。   感兴趣的朋友可以去我的 Github 下载相关代码,如果对你有所帮助,不妨 Star 一下,谢谢大家支持! ———————————————— 版权声明:本文为CSDN博主「dovienson」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/pipidog008/article/details/128869522 ​
  • [其他] Spring探索|既生@Resource,何生@Autowired?
    提到Spring依赖注入,大家最先想到应该是@Resource和@Autowired,很多文章只是讲解了功能上的区别,对于Spring为什么要支持两个这么类似的注解却未提到,属于知其然而不知其所以然。不知大家在使用这两个注解的时候有没有想过,@Resource又支持名字又支持类型,还要@Autowired干嘛,难道是Spring官方没事做了?真的是没事做了吗?读了本文你将会了解到:1.@Resource和@Autowired来源2.Spring官方为什么会支持这两个功能如此相似的注解?3.为什么@Autowired属性注入的时候Idea会曝出黄色的警告?4.@Resource和@Autowired推荐用法来源既然要弄清楚,就要先了解他们的身世。@Resource 于 2006年5月11日随着JSR 250 发布 ,官方解释是:Resource 注释标记了应用程序需要的资源。该注解可以应用于应用程序组件类,或组件类的字段或方法。当注解应用于字段或方法时,容器将在组件初始化时将所请求资源的实例注入到应用程序组件中。如果注释应用于组件类,则注释声明应用程序将在运行时查找的资源。可以看到它类似一个定义,而由其他的组件或框架自由实现。@Autowired 于 2007年11月19日随着Spring2.5发布,同时官方也对@Resource进行了支持。@Autowired的官方解释是:将构造函数、字段、设置方法或配置方法标记为由 Spring 的依赖注入工具自动装配。可以看到,@Autowired 是 Spring的亲儿子,而@Resource是Spring对它定义的一种实现,它们的功能如此相似。那么为什么要支持了@Resource,又要自己搞个@Autowired呢?对此专门查了一下Spring2.5的官方文档,文档中有一段这么说到:However, Spring 2.5 dramatically changes the landscape. As described above, the autowiring choices have now been extended with support for the JSR-250 @Resource annotation to enable autowiring of named resources on a per-method or per-field basis. However, the @Resource annotation alone does have some limitations. Spring 2.5 therefore introduces an @Autowired annotation to further increase the level of control.大概的意思是说,Spring2.5 支持注解自动装配啦, 现已经支持JSR-250 @Resource 基于每个方法或每个字段的命名资源的自动装配,但是只有@Resource是不行的,我们还推出了“粒度”更大的@Autowired,来覆盖更多场景了。嗯哼,那么官方说的“粒度”就是关键了,那“粒度”指的是什么呢”?既生“@Resource”,何生“@Autowired”要想找到粒度是什么,我们先从两个注解的功能下手@Autowired类型注入@Resource名字注入优先,找不到名字找类型论功能的“粒度”,@Resource已经包含@Autowired了啊,“粒度”更大啊,难道是Spring2.5的时候还不是这样?我又去翻了下Spring2.5文档,上面明确的写到:When using @Resource without an explicitly provided name, if no Spring-managed object is found for the default name, the injection mechanism will fallback to a type-match.这不是和现在一样的吗,我此时凌乱了。那么“粒度”到底指的是什么?在混迹众多论坛后,其中stackoverflow的一段话引起了我的注意:Both @Autowired and @Resource work equally well. But there is a conceptual difference or a difference in the meaning.@Resource means get me a known resource by name. The name is extracted from the name of the annotated setter or field, or it is taken from the name-Parameter.@Inject or @Autowired try to wire in a suitable other component by type.So, basically these are two quite distinct concepts. Unfortunately the Spring-Implementation of @Resource has a built-in fallback, which kicks in when resolution by-name fails. In this case, it falls back to the @Autowired-kind resolution by-type. While this fallback is convenient, IMHO it causes a lot of confusion, because people are.大概的意思是:Spring虽然实现了两个功能类似的,但是存在概念上的差异或含义上的差异:@Resource 这按名称给我一个确定已知的资源。@Autowired 尝试按类型连接合适的其他组件。但是@Resource当按名称解析失败时会启动。在这种情况下,它会按类型解析,引起概念上的混乱,因为开发者没有意识到概念上的差异,而是倾向于使用@Resource基于类型的自动装配。原来Spring官方说的“粒度”是指“资源范围”,@Resource找寻的是确定的已知的资源,相当于给你一个坐标,你直接去找。@Autowired是在一片区域里面尝试搜索合适的资源。所以上面的问题答案已经基本明确了。Spring为什么会支持两个功能相似的注解呢?它们的概念不同,@Resource更倾向于找已知资源,而Autowired倾向于尝试按类型搜索资源。方便其他框架迁移,@Resource是一种规范,只要符合JSR-250规范的其他框架,Spring就可以兼容。既然@Resource更倾向于找已知资源,为什么也有按类型注入的功能?个人猜测:可能是为了兼容从Spring切换到其他框架,开发者就算只使用Resource也是保持Spring强大的依赖注入功能。Spring 的区别对待看到这相信大家对使用@Resource还是@Autowired有了自己的见解。在日常写代码中有个小细节不知道大家有没有注意到,使用@Autowired在属性上的时候Idea会曝出黄色的警告,并且推荐我们使用构造方法注入,而Resource就不会,这是为什么呢?警告如下:为什么@Autowired在属性上的时候Idea会曝出黄色的警告,并且推荐我们使用构造方法注入?其实Spring文档中已经给出了答案,主要有这几点:1 声明不了常量的属性基于属性的依赖注入不适用于声明为 final 的字段,因为此字段必须在类实例化时去实例化。声明不可变依赖项的唯一方法是使用基于构造函数的依赖项注入。2 容易忽视类的单一原则一个类应该只负责软件应用程序功能的单个部分,并且它的所有服务都应该与该职责紧密结合。如果使用属性的依赖注入,在你的类中很容易有很多依赖,一切看起来都很正常。但是如果改用基于构造函数的依赖注入,随着更多的依赖被添加到你的类中,构造函数会变得越来越大,代码开始就开始出现“异味”,发出明确的信号表明有问题。具有超过十个参数的构造函数清楚地表明该类有太多的依赖,让你不得不注意该类的单一问题了。因此,属性注入虽然不直接打破单一原则,但它却可以帮你忽视单一原则。3 循环依赖问题A类通过构造函数注入需要B类的实例,B类通过构造函数注入需要A类的实例。如果你为类 A 和 B 配置 bean 以相互注入,使用构造方法就能很快发现。4 依赖注入强依赖Spring容器如果您想在容器之外使用这的类,例如用于单元测试,不得不使用 Spring 容器来实例化它,因为没有其他可能的方法(除了反射)来设置自动装配的字段。为什么@Resource没有呢?在官方文档中,我没有找到答案,查了一些资料说是:@Autowired 是 Spring 提供的,一旦切换到别的 IoC 框架,就无法支持注入了. 而@Resource 是 JSR-250 提供的,它是 Java 标准,我们使用的 IoC 容器应该和它兼容,所以即使换了容器,它也能正常工作。@Autowired和@Resource推荐用法1. 什么场景用什么合适记住一句话就行,@Resource倾向于确定性的单一资源,@Autowired为类型去匹配符合此类型所有资源。如集合注入,@Resource也是可以的,但是建议使用@Autowired。idea左侧的小绿标可以看出来,不建议使用@Resource注入集合资源,本质上集合注入不是单一,也是不确定性的。2 @Autowired推荐用法方法1 :使用构造函数注入(推荐)原生版:优雅版:使用lombok的@RequiredArgsConstructor+private final方法2:set注入原生版:优雅版:使用lombok的@Setter来源:https://mp.weixin.qq.com/s/anNwFO0LY4qyGN7QKdCNpQ
  • [SQL] Spring 事务失效的常见原因及解决方案
    Spring 事务失效的常见原因1.数据库引擎不支持事务:某些数据库引擎(如MyISAM)不支持事务,如果使用这些引擎,则无法使用Spring事务。 2.事务传播机制设置不正确:事务传播机制(Propagation)是指当一个事务方法调用另一个事务方法时,如何处理事务的传播。如果事务传播机制设置不正确,可能会导致事务失效。 3.未捕获异常:当一个未捕获的异常被抛出时,Spring默认会回滚事务,但是如果在方法中捕获了异常并且没有抛出,那么事务就不会回滚。 4.没有使用代理对象:Spring的事务管理是通过代理实现的,如果在调用事务方法时没有使用代理对象,那么事务就不会生效。 5.同一类中的事务方法之间相互调用:当同一个类中的方法相互调用时,Spring默认不会使用代理对象,因此事务也不会生效。 6.数据库隔离级别不正确:数据库的隔离级别会影响事务的提交和回滚。如果数据库隔离级别设置不正确,可能会导致事务失效。常见事务失效的解决方案确认数据库引擎是否支持事务。确认事务传播机制是否设置正确。确认方法中是否捕获异常并处理。确认是否使用了代理对象来调用事务方法。确认是否在同一个类中相互调用事务方法。确认数据库隔离级别是否正确。
  • [问题求助] 怎么理解spring里面的ioc呢 ?
    如题,大佬们通俗的讲讲