• [技术干货] 当注入的 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呢 ?
    如题,大佬们通俗的讲讲
  • [问题求助] 求助,spring @resource和@autowired的区别
    大佬们能不能简单通俗的说下spring @resource和@autowired的区别,通俗易懂最好,搜索引擎有的看不懂,
  • [技术干货] 为什么有人不推荐使用spring官方推荐的@Transactional声明式注解
     事务管理在系统开发中是不可缺少的一部分,Spring 提供了很好事务管理机制,主要分为编程式事务和声明式事务两种。  关于事务的基础知识,如什么是事务,数据库事务以及 Spring 事务的 ACID、隔离级别、传播机制、行为等,就不在这篇文章中详细介绍了。默认大家都有一定的了解。  本文,作者会先简单介绍下什么是声明式事务和编程式事务,再说一下为什么我不建议使用声明式事务。  编程式事务 基于底层的 API,如 PlatformTransactionManager、TransactionDefinition 和 TransactionTemplate 等核心接口,开发者完全可以通过编程的方式来进行事务管理。  编程式事务方式需要是开发者在代码中手动的管理事务的开启、提交、回滚等操作。  public void test() {       TransactionDefinition def = new DefaultTransactionDefinition();       TransactionStatus status = transactionManager.getTransaction(def);         try {          // 事务操作          // 事务提交          transactionManager.commit(status);       } catch (DataAccessException e) {          // 事务提交          transactionManager.rollback(status);          throw e;       } } 如以上代码,开发者可以通过 API 自己控制事务。  声明式事务 声明式事务管理方法允许开发者配置的帮助下来管理事务,而不需要依赖底层 API 进行硬编码。开发者可以只使用注解或基于配置的 XML 来管理事务。  @Transactional public void test() {      // 事务操作   } 如上,使用 @Transactional 即可给 test 方法增加事务控制。  当然,上面的代码只是简化后的,想要使用事务还需要一些配置内容。这里就不详细阐述了。  这两种事务,格子有各自的优缺点,那么,各自有哪些适合的场景呢?为什么有人会拒绝使用声明式事务呢?  声明式事务的优点 通过上面的例子,其实我们可以很容易的看出来,声明式事务帮助我们节省了很多代码,他会自动帮我们进行事务的开启、提交以及回滚等操作,把程序员从事务管理中解放出来。  声明式事务管理使用了 AOP 实现的,本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。  使用这种方式,对代码没有侵入性,方法内只需要写业务逻辑就可以了。  但是,声明式事务真的有这么好么?倒也不见得。  声明式事务的粒度问题 首先,声明式事务有一个局限,那就是他的最小粒度要作用在方法上。  也就是说,如果想要给一部分代码块增加事务的话,那就需要把这个部分代码块单独独立出来作为一个方法。  但是,正是因为这个粒度问题,本人并不建议过度的使用声明式事务。  首先,因为声明式事务是通过注解的,有些时候还可以通过配置实现,这就会导致一个问题,那就是这个事务有可能被开发者忽略。  事务被忽略了有什么问题呢?  首先,如果开发者没有注意到一个方法是被事务嵌套的,那么就可能会再方法中加入一些如 RPC 远程调用、消息发送、缓存更新、文件写入等操作。  我们知道,这些操作如果被包在事务中,有两个问题:  1、这些操作自身是无法回滚的,这就会导致数据的不一致。可能 RPC 调用成功了,但是本地事务回滚了,可是 PRC 调用无法回滚了。  2、在事务中有远程调用,就会拉长整个事务。那么久会导致本事务的数据库连接一直被占用,那么如果类似操作过多,就会导致数据库连接池耗尽。  有些时候,即使没有在事务中进行远程操作,但是有些人还是可能会不经意的进行一些内存操作,如运算。或者如果遇到分库分表的情况,有可能不经意间进行跨库操作。  但是如果是编程式事务的话,业务代码中就会清清楚楚看到什么地方开启事务,什么地方提交,什么时候回滚。这样有人改这段代码的时候,就会强制他考虑要加的代码是否应该方法事务内。  有些人可能会说,已经有了声明式事务,但是写代码的人没注意,这能怪谁。  话虽然是这么说,但是我们还是希望可以通过一些机制或者规范,降低这些问题发生的概率。  比如建议大家使用编程式事务,而不是声明式事务。因为,作者工作这么多年来,发生过不止一次开发者没注意到声明式事务而导致的故障。  因为有些时候,声明式事务确实不够明显。  声明式事务用不对容易失效 除了事务的粒度问题,还有一个问题那就是声明式事务虽然看上去帮我们简化了很多代码,但是一旦没用对,也很容易导致事务失效。  如以下几种场景就可能导致声明式事务失效:  1、@Transactional 应用在非 public 修饰的方法上 2、@Transactional 注解属性 propagation 设置错误 3、@Transactional 注解属性 rollbackFor 设置错误 4、同一个类中方法调用,导致 @Transactional 失效 5、异常被 catch 捕获导致 @Transactional 失效 6、数据库引擎不支持事务  以上几个问题,如果使用编程式事务的话,很多都是可以避免的。  使用声明事务失效的问题我们发生过很多次。不知道大家有没有遇到过,我是实际遇到过的  因为 Spring 的事务是基于 AOP 实现的,但是在代码中,有时候我们会有很多切面,不同的切面可能会来处理不同的事情,多个切面之间可能会有相互影响。  在之前的一个项目中,我就发现我们的 Service 层的事务全都失效了,一个 SQL 执行失败后并没有回滚,排查下来才发现,是因为一位同事新增了一个切面,这个切面里面做个异常的统一捕获,导致事务的切面没有捕获到异常,导致事务无法回滚。  这样的问题,发生过不止一次,而且不容易被发现。  很多人还是会说,说到底还是自己能力不行,对事务理解不透彻,用错了能怪谁。  但是我还是那句话,我们确实无法保证所有人的能力都很高,也无法要求所有开发者都能不出错。我们能做的就是,尽量可以通过机制或者规范,来避免或者降低这些问题发生的概率。  其实,如果大家有认真看过阿里巴巴出的那份 Java 开发手册的话,其实就能发现,其中的很多规约并不是完完全全容易被人理解,有些也比较生硬,但是其实,这些规范都是从无数个坑里爬出来的开发者们总结出来的。  关于 @Transactional 的用法,规约中也有提到过,只不过规约中的观点没有我这么鲜明:   总结 最后,相信本文的观点很多人都并不一定认同,很多人会说:Spring 官方都推荐无侵入性的声明式事务,你有啥资格出来 BB 。  说实话,刚工作的前几年,我也热衷于使用声明式事务,觉得很干净,也很” 优雅”。觉得师兄们使用编程式事务多此一举,没有工匠精神。  但是慢慢的,线上发生过几次问题之后,我们复盘后发现,很多时候你自己写的代码很优雅,这完全没问题。  但是,优雅的同时也带来了一些副作用,师兄们又不能批评我,因为我的用法确实没错…  所以,有些事,还是要痛过之后才知道。  当然,本文并不要求大家一定要彻底不使用声明式事务,只是建议大家日后在使用事务的时候,能够考虑到本文中提到的观点,然后自行选择。  ———————————————— 原文作者:zhaozhangxiao 转自链接:https://learnku.com/articles/69225  
  • [技术干货] Spring | Bean自动装配详解-转载
     Bean的自动装配 5.1、自动装配说明 自动装配是使用spring满足bean依赖的一种方法  spring会在应用上下文中为某个bean寻找其依赖的bean。  Spring中bean有三种装配机制,分别是:  在xml中显式配置; 在java中显式配置; 隐式的bean发现机制和自动装配。【重要】 Spring的自动装配需要从两个角度来实现,或者说是两个操作: 组件扫描(component scanning):spring会自动发现应用上下文中所创建的bean; 自动装配(autowiring):spring自动满足bean之间的依赖,也就是我们说的IoC/DI; 组件扫描和自动装配组合发挥巨大威力,使得显示的配置降低到最少。  推荐不使用自动装配xml配置 , 而使用注解 .  5.2、测试环境搭建 1、新建一个项目  2、新建两个实体类,Cat Dog 都有一个叫的方法  public class Cat {    public void shout() {        System.out.println("miao~");   } } public class Dog {    public void shout() {        System.out.println("wang~");   } } 3、新建一个用户类 User  public class User {    private Cat cat;    private Dog dog;    private String name; } 4、编写Spring配置文件  <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        xmlns:p="http://www.springframework.org/schema/p"        xmlns:c="http://www.springframework.org/schema/c"        xsi:schemaLocation="http://www.springframework.org/schema/beans         https://www.springframework.org/schema/beans/spring-beans.xsd">      <bean id="cat" class="com.bby.pojo.Cat"/>     <bean id="dog" class="com.bby.pojo.Dog"/>     <bean id="user" class="com.bby.pojo.User">         <property name="name" value="啵啵鱼"/>         <property name="cat" ref="cat"/>         <property name="dog" ref="dog"/>     </bean> </beans> 5、测试  public class UserTest {     @Test     public void test() {         ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");         User user = context.getBean("user", User.class);         System.out.println(user);         user.getCat().shout();         user.getDog().shout();     } } 5.3、 自动装配(autowire) 5.3.1、byName (按名称自动装配) 由于在手动配置xml过程中,常常发生字母缺漏和大小写等错误,而无法对其进行检查,使得开发效率降低。  采用自动装配将避免这些错误,并且使配置简单化。  测试:  1、修改bean配置,增加一个属性 autowire=“byName”  <bean id="user" class="com.bby.pojo.User" autowire="byName">    <property name="name" value="啵啵鱼"/> </bean> 1 2 3 2、再次测试,结果依旧成功输出!  3、我们将 cat 的bean id修改为 catXXX  4、再次测试, 执行时报空指针java.lang.NullPointerException。因为按byName规则找不对应set方法,真正的setCat就没执行,对象就没有初始化,所以调用时就会报空指针错误。  小结:  当一个bean节点带有 autowire byName的属性时。  将查找其类中所有的set方法名,例如setCat,获得将set去掉并且首字母小写的字符串,即cat。  去spring容器中寻找是否有此字符串名称id的对象。  如果有,就取出注入;如果没有,就报空指针异常。  5.3.2、byType (按类型自动装配) 使用autowire byType首先需要保证:同一类型的对象,在spring容器中唯一。如果不唯一,会报不唯一的异常。  NoUniqueBeanDefinitionException 1 测试:  1、将user的bean配置修改一下 : autowire=“byType”  2、测试,正常输出  3、在注册一个cat 的bean对象!  <bean id="dog" class="com.bby.pojo.Dog"/> <bean id="cat" class="com.bby.pojo.Cat"/> <bean id="cat2" class="com.bby.pojo.Cat"/>  <bean id="user" class="com.bby.pojo.User" autowire="byType">    <property name="str" value="啵啵鱼"/> </bean> 4、测试,报错:NoUniqueBeanDefinitionException  5、删掉cat2,将cat的bean名称改掉!测试!因为是按类型装配,所以并不会报异常,也不影响最后的结果。甚至将id属性去掉,也不影响结果。  5.3.3、使用注解 jdk1.5开始支持注解,spring2.5开始全面支持注解。  (1)准备工作 1、在spring配置文件中引入context文件头  xmlns:context="http://www.springframework.org/schema/context"      http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd 1 2 3 4 2、开启属性注解支持!  <context:annotation-config/> 1  (2)@Autowired 直接在属性上使用即可!也可以再set方式上使用!  使用AutoWired我们可以不用编写Set方法了,前提是这个自动装配的属性在IOC(Spring) 容器中存在  @Autowired默认使用 byType来装配属性,如果匹配到类型的多个实例,再通过 byName来确定 bean 需要导入 spring-aop的包! 测试:  1、将User类中的set方法去掉,使用@Autowired注解  public class User {     private String name;     @Autowired     private Cat cat;     @Autowired     private Dog dog;      public String getName() {         return name;     }      public Cat getCat() {         return cat;     }      public Dog getDog() {         return dog;     }      @Override     public String toString() {         return "User{" +                 "name='" + name + '\'' +                 ", cat=" + cat +                 ", dog=" + dog +                 '}';     } } 科普:  @Nullable //字段标记了这个注解,说明这个字段可以为null 1 //如果允许对象为null,设置required = false,默认为true @Autowired(required = false) private Cat cat; (3)@Qualifier 加上@Qualifier可以根据byName的方式自动装配 @Qualifier不能单独使用。 测试实验步骤:  1、配置文件修改内容,保证类型存在对象。且名字不为类的默认名字(默认名字为类的小写 如:Dog类名字为dog)!  <bean id="dog1" class="com.bby.pojo.Dog"/> <bean id="dog2" class="com.bby.pojo.Dog"/> <bean id="cat1" class="com.bby.pojo.Cat"/> <bean id="cat2" class="com.bby.pojo.Cat"/> 2、没有加Qualifier测试,直接报错  3、在属性上添加Qualifier注解  @Autowired @Qualifier(value = "cat2") private Cat cat; @Autowired @Qualifier(value = "dog2") private Dog dog; 测试,成功输出!  (4)@Resource @Resource如有指定的name属性,先按该属性进行byName方式查找装配; 其次再进行默认的byName方式进行装配; 如果以上都不成功,则按byType的方式自动装配。 都不成功,则报异常。 实体类:  public class User {    //如果允许对象为null,设置required = false,默认为true    @Resource(name = "cat2")    private Cat cat;    @Resource    private Dog dog;    private String str; } beans.xml  <bean id="dog" class="com.bby.pojo.Dog"/> <bean id="cat1" class="com.bby.pojo.Cat"/> <bean id="cat2" class="com.bby.pojo.Cat"/>  <bean id="user" class="com.bby.pojo.User"/> 1 2 3 4 5 测试:结果OK  配置文件2:beans.xml , 删掉cat2  <bean id="dog" class="com.bby.pojo.Dog"/> <bean id="cat1" class="com.bby.pojo.Cat"/> 1 2 实体类上只保留注解  @Resource private Cat cat; @Resource private Dog dog; 1 2 3 4 结果:OK  结论:先进行byName查找,失败;再进行byType查找,成功。  (5)@Resource 和 @Autowired的区别 都是用来自动装配的,都可以放在属性字段上 @Autowired默认通过byType 的方式,当匹配到多个同类型时,使用byName进行装配,默认情况下必须要求依赖对象必须存在,如果要允许null 值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用 @Resource(属于J2EE复返)默认通过byName的方式实现,如果找不到名字,则通过byType实现!如果name属性一旦指定,就只会按照名称进行装配。如果两个都找不到就报错 它们的作用相同都是用注解方式注入对象,但执行顺序不同,@Autowired先byType,@Resource先byName。 ———————————————— 版权声明:本文为CSDN博主「-BoBooY-」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_58233406/article/details/127331116 
  • [技术干货] 【Spring Cloud】项目优化:如何确保Redis延迟队列中数据能够被正确消费-转载
     一:回顾梳理 1.流程图          完成前面的操作之后,使用延迟队列定时提交文章功能就算完成了。在测试过程中我发现一个问题,虽然数据做了持久化处理,但是当每次消费任务之后数据库中该条数据也会随之被清理掉,这时候还会存在数据丢失的风险。为什么这么说呢,我们的定时发布是按照下面的流程图进行的:   2.代码 对应的代码如下:  @Autowired private WmNewsAutoScanServiceImpl wmNewsAutoScanService;   /**      * 消费延迟队列数据      */ @Scheduled(fixedRate = 1000) @Override @SneakyThrows public void scanNewsByTask() {       ResponseResult responseResult = scheduleClient.poll(TaskTypeEnum.NEWS_SCAN_TIME.getTaskType(), TaskTypeEnum.NEWS_SCAN_TIME.getPriority());     if(responseResult.getCode().equals(200) && responseResult.getData() != null){         log.info("文章审核---消费任务执行---begin---");           String json_str = JSON.toJSONString(responseResult.getData());         Task task = JSON.parseObject(json_str, Task.class);         byte[] parameters = task.getParameters();         WmNews wmNews = ProtostuffUtil.deserialize(parameters, WmNews.class);         System.out.println(wmNews.getId()+"-----------");         wmNewsAutoScanService.autoScanWmNews(wmNews.getId());           log.info("文章审核---消费任务执行---end---");       }      }      /**      * 删除任务,更新日志      * @param taskId      * @param status      * @return      */     private Task UpdateDb(long taskId, int status) {         Task task = null;         try {             //1.删除任务             log.info("删除数据库中的任务...");             taskInfoMapper.deleteById(taskId);               //2.更新日志             log.info("更新任务日志...");             TaskinfoLogs taskinfoLogs = taskInfoLogsMapper.selectById(taskId);             taskinfoLogs.setStatus(status);             taskInfoLogsMapper.updateById(taskinfoLogs);               //3.设置返回值             task = new Task();             BeanUtils.copyProperties(taskinfoLogs,task);             task.setExecuteTime(taskinfoLogs.getExecuteTime().getTime());         } catch (BeansException e) {             throw new RuntimeException(e);         }           return task;     }       /**      * 消费任务      * @param type  任务类型      * @param priority 任务优先级      * @return  Task      */     @Override     public Task poll(int type, int priority) {         Task task = null;         try {             String key = type + "_" + priority;               String task_json = cacheService.lRightPop(ScheduleConstants.TOPIC + key);             if(StringUtils.isNotBlank(task_json)) {                 task = JSON.parseObject(task_json,Task.class);                 //更新数据库                 UpdateDb(task.getTaskId(),ScheduleConstants.EXECUTED);             }         } catch (Exception e) {             e.printStackTrace();             log.error("poll task exception");         }         return task;     }          可以看到在redis中获取数据之后便将数据从数据库中删除,这时候假如后面的审核流程出现问题或者保存文章时候移动端微服务出现故障导致文章不能保存,而这时候数据库中及redis中的数据都删除了,这就造成了数据的丢失。  二:第一次优化 1.优化策略         首先我想到的优化策略是当检测到文章审核或者文章保存值移动端有异常时候就将已经出队列的数据重新放回队列并且在5分钟之后再进行消费直到消费成功,流程图见下图:   2.代码实现 WmAutoScanServiceImpl  package com.my.wemedia.service.impl;   @Slf4j @Service @Transactional public class WmAutoScanServiceImpl implements WmAutoScanService {     @Autowired     private WmNewsService wmNewsService;     @Autowired     private TextDetection textDetection;     @Autowired     private ImageDetection imageDetection;       /**      * 自动审核文章文本及图片      * @param id      */     @Override     @Async     public void AutoScanTextAndImage(Integer id) throws Exception {         log.info("开始进行文章审核...");         // Thread.sleep(300);    //休眠300毫秒,以保证能够获取到数据库中的数据         WmNews wmNews = wmNewsService.getById(id);         if(wmNews == null) {             throw new RuntimeException("WmAutoScanServiceImpl-文章信息不存在");         }           if(wmNews.getStatus().equals(WmNews.Status.SUBMIT.getCode())) {             //中间步骤省略               //4,审核成功             //4.1保存文章             log.info("检测到文章无违规内容");             ResponseResult responseResult = saveAppArticle(wmNews);             if(!responseResult.getCode().equals(200)) {                 throw new RuntimeException("AutoScanTextAndImage--检测失败");             }           }     }         /**      * 保存App端文章数据      * @param wmNews      * @return      */     @Override     public ResponseResult saveAppArticle(WmNews wmNews) {         //中间步骤省略           //7.保存App端文章         log.info("异步调用保存文章至App端");         ResponseResult responseResult = null;         try {              responseResult = iArticleClient.saveArticle(articleDto);         } catch (Exception e) {             responseResult = new ResponseResult(AppHttpCodeEnum.SERVER_ERROR.getCode(),"保存文章至App失败");         }           return responseResult;     } }   WmNewsTaskServiceImpl      @Autowired     private WmAutoScanService wmAutoScanService;     @Autowired     private CacheService cacheService;     /**      * 消费任务      */     @Override     @Scheduled(fixedRate = 2000)  //每两秒执行一次     public void scanNewsByTask() {         ResponseResult responseResult = scheduleClient.poll(TaskTypeEnum.NEWS_SCAN_TIME.getTaskType(), TaskTypeEnum.NEWS_SCAN_TIME.getPriority());         if(responseResult.getCode().equals(200) && responseResult.getData() != null) {             log.info("文章审核---消费任务执行---begin");               String json_str = JSON.toJSONString(responseResult.getData());             Task task = JSON.parseObject(json_str,Task.class);             byte[] parameters = task.getParameters();             //反序列化             WmNews wmNews = ProtostuffUtil.deserialize(parameters, WmNews.class);               try {                 wmAutoScanService.AutoScanTextAndImage(wmNews.getId());             } catch (Exception e) {                 log.warn("审核失败,将于5分钟之后再次尝试");                 //文章未能成功审核,将数据加入ZSet,5分钟之后再重新尝试                 //1.构造key                 String key = task.getTaskType() + "_" + task.getPriority();                 //2.获取5分钟之后的时间                 Calendar calendar = Calendar.getInstance();                 calendar.add(Calendar.MINUTE,5);                 long nextScheduleTime = calendar.getTimeInMillis();                   //3.将数据重新加入                 cacheService.zAdd(ScheduleConstants.FUTURE + key,JSON.toJSONString(task),nextScheduleTime);                 e.printStackTrace();             }               log.info("文章审核---消费任务执行---end");         }  3.遇到的问题         这时候虽然做了优化,但是在实际运行时候 WmNewsTaskServiceImpl中的try-catch并没有发挥作用,我没有开启移动端微服务,在审核时候也抛出了异常,但是这时候WmNewsTaskServiceImpl中并没有catch到这个异常。这是为什么呢,通过断点发现在执行到AutoScanTextAndImage方法时候,程序便直接跳出了catch块,这时候便想到前面我们还没有引入延迟队列时候用的是异步方法来调用文本图片审核方法来进行审核,而且当时还出现了在数据库中获取不到数据的问题,现在想起这就是因为采用了异步调用才引起的。在这里也一样,由于在AutoScanTextAndImage方法前面添加了@Async注解,表明这是一个异步方法,这时候我们在 WmNewsTaskServiceImpl中调用该方法时候是异步执行的,这当然就捕获不到抛出的异常,因需要将该方法的@Async注解去掉,改成同步调用。   三:第二次优化 1.问题引入 问题一:         解决完上面的问题,看着似乎问题得到了解决,消息没有正确被消费时候会被重新投递回去直到被正确消费,但是这时候还应该注意到另外一个问题。虽然消息消费失败之后被重新投递了,但是这时候数据库中的数据已经被删除掉了,假如redis服务器出现了问题,这时候就算你采用了重回队列策略数据还是永久丢失了,因为你的持久化处理在这时候已经失效了。这时候可以考虑失败之后将数据再存回数据库中,这样再次做了持久化处理,但是这样显然会造成不必要的IO操作。  问题二:         前面的做法是将审核的方法由异步改成了同步,这时候由于调用的是第三方的审核接口,有时候难免会因为网络等原因造成审核时间很长,这时候假如采用同步策略,就会造成长时间阻塞,影响用户体验,同时也会浪费大量资源。   2.解决策略 针对问题一,我认为在一开始对队列消息进行消费时候就不应该立马删除数据库中的数据,而是等到最后确保消息被正确处理之后再删除数据库中相应的数据。 针对问题二,我认为不应该将审核方法由异步改成同步,但是这时候就会出现前面提到的问题----catch不到异常,这时候似乎又回到了起点。试想,我们使用这个异常捕获的目的是什么?我们的目的就是为了出现异常时候将消息重新入队防止数据丢失,这时候我们不妨换一种策略,前面提到了我们在确保消息被正确消费之后再删除数据库中的数据,这不就已经解决了问题了吗?我们会定时同步数据库中的数据到redis,这时候就算消息在redis中丢失了也没关系,只要数据库中的数据还在就行。  流程图见下图:   3.代码实现 ①在tbug-headlines-feign-api模块的IScheduleClient接口中添加如下内容:      /**      * 删除数据库中的任务,更新日志      * @param taskId      */     @DeleteMapping("/api/v1/task/delete/{taskId}/{status}")     void updateDb(@PathVariable("taskId") Long taskId, @PathVariable("status") Integer status); ②在package com.my.schedule.feign中更新该接口的实现类      /**      * 删除数据库中任务,更新日志      * @param taskId      */     @Override     @DeleteMapping("/api/v1/task/delete/{taskId}/{status}")     public void updateDb(@PathVariable("taskId")Long taskId, @PathVariable("status") Integer status) {         taskService.UpdateDb(taskId,status);     } ③在TaskService中增加UpdateDb(long taskId, int status)方法,将实现类中该方法权限设置为public  TaskService      /**      * 删除数据库任务并更新日志      * @param taskId      * @param status      * @return      */     Task UpdateDb(long taskId, int status); Impl      /**      * 删除任务,更新日志      * @param taskId      * @param status      * @return      */     public Task UpdateDb(long taskId, int status) {         Task task = null;         try {             //1.删除任务             log.info("删除数据库中的任务...");             taskInfoMapper.deleteById(taskId);               //2.更新日志             log.info("更新任务日志...");             TaskinfoLogs taskinfoLogs = taskInfoLogsMapper.selectById(taskId);             taskinfoLogs.setStatus(status);             taskInfoLogsMapper.updateById(taskinfoLogs);               //3.设置返回值             task = new Task();             BeanUtils.copyProperties(taskinfoLogs,task);             task.setExecuteTime(taskinfoLogs.getExecuteTime().getTime());         } catch (BeansException e) {             throw new RuntimeException(e);         }           return task;     }  ④修改taskServiceImpl中的poll方法      /**      * 消费任务      * @param type  任务类型      * @param priority 任务优先级      * @return  Task      */     @Override     public Task poll(int type, int priority) {         Task task = null;         try {             String key = type + "_" + priority;               String task_json = cacheService.lRightPop(ScheduleConstants.TOPIC + key);             if(StringUtils.isNotBlank(task_json)) {                 task = JSON.parseObject(task_json,Task.class);                 //更新数据库   (抛弃该策略)                 // UpdateDb(task.getTaskId(),ScheduleConstants.EXECUTED);                   //接口幂等性                 TaskinfoLogs taskinfoLogs = taskInfoLogsMapper.selectById(task.getTaskId());                 //获取任务状态                 if(taskinfoLogs != null) {                     Integer status = taskinfoLogs.getStatus();                     if(ScheduleConstants.EXECUTED == status) {                         return null;                     }                 }             }         } catch (Exception e) {             e.printStackTrace();             log.error("poll task exception");         }         return task;     }  这里主要修改了两点:  抛弃原来直接调用方法删除数据库中的任务的策略 增加接口幂等性,假如该任务已经被成功执行但是并没有在数据库中删除该任务,那么第二次执行该任务时候假如判断到该任务已经执行过则直接返回null不做处理。 ⑤修改自动审核方法      @Autowired     private IScheduleClient scheduleClient;       /**      * 自动审核文章文本及图片      * @param id      */     @Override     @Async     public void AutoScanTextAndImage(Integer id,Long taskId) throws Exception {         log.info("开始进行文章审核...");         // Thread.sleep(300);    //休眠300毫秒,以保证能够获取到数据库中的数据         WmNews wmNews = wmNewsService.getById(id);         if(wmNews == null) {             throw new RuntimeException("WmAutoScanServiceImpl-文章信息不存在");         }           if(wmNews.getStatus().equals(WmNews.Status.SUBMIT.getCode())) {             //1.提取文章文本及图片             Map map = getTextAndImages(wmNews);               //2.检测文本             //2.1提取文本             String content = ((StringBuilder) map.get("content")).toString();             //2.2自管理敏感词过滤             boolean SHandleResult = handleSensitiveScan(content,wmNews);             if(!SHandleResult) return;             //2.3调用腾讯云进行文本检测             Boolean THandleResult = handleTextScan(content, wmNews);             if(!THandleResult) return;               //3.检测图片             //3.1提取图片             List imageUrl = (List) map.get("images");             //3.2调用腾讯云对图片进行检测             Boolean IHandleResult = handleImageScan(imageUrl, wmNews);             if(!IHandleResult) return;               //4,审核成功             //4.1保存文章             log.info("检测到文章无违规内容");             ResponseResult responseResult = saveAppArticle(wmNews);             if(!responseResult.getCode().equals(200)) {                 throw new RuntimeException("AutoScanTextAndImage--检测失败");             }               //4.2回填article_id             wmNews.setArticleId((Long) responseResult.getData());             wmNews.setStatus(WmNews.Status.PUBLISHED.getCode());             wmNews.setReason("审核成功");             wmNewsService.updateById(wmNews);               //删除数据库中的任务并更新日志             scheduleClient.updateDb(taskId, ScheduleConstants.EXECUTED);         }     }  ———————————————— 版权声明:本文为CSDN博主「赵四司机」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_45750572/article/details/126453285 
总条数:142 到第
上滑加载中