-
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
-
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框架中常用的设计模式包括:工厂方法模式(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 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层存在的意义了。
-
目前最新版的 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
-
前言 上篇文章我们介绍了 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官方没事做了?真的是没事做了吗?读了本文你将会了解到: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
-
Spring 事务失效的常见原因1.数据库引擎不支持事务:某些数据库引擎(如MyISAM)不支持事务,如果使用这些引擎,则无法使用Spring事务。 2.事务传播机制设置不正确:事务传播机制(Propagation)是指当一个事务方法调用另一个事务方法时,如何处理事务的传播。如果事务传播机制设置不正确,可能会导致事务失效。 3.未捕获异常:当一个未捕获的异常被抛出时,Spring默认会回滚事务,但是如果在方法中捕获了异常并且没有抛出,那么事务就不会回滚。 4.没有使用代理对象:Spring的事务管理是通过代理实现的,如果在调用事务方法时没有使用代理对象,那么事务就不会生效。 5.同一类中的事务方法之间相互调用:当同一个类中的方法相互调用时,Spring默认不会使用代理对象,因此事务也不会生效。 6.数据库隔离级别不正确:数据库的隔离级别会影响事务的提交和回滚。如果数据库隔离级别设置不正确,可能会导致事务失效。常见事务失效的解决方案确认数据库引擎是否支持事务。确认事务传播机制是否设置正确。确认方法中是否捕获异常并处理。确认是否使用了代理对象来调用事务方法。确认是否在同一个类中相互调用事务方法。确认数据库隔离级别是否正确。
-
如题,大佬们通俗的讲讲
-
大佬们能不能简单通俗的说下spring @resource和@autowired的区别,通俗易懂最好,搜索引擎有的看不懂,
-
事务管理在系统开发中是不可缺少的一部分,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
-
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
-
一:回顾梳理 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
-
什么是数据?1.所有能被输入到计算机中,且能被计算机处理的符号的总称。如:实数、整数、字符(串)、图形和声音等。2.是计算机操作对象的集合。3.是计算机处理的信息的某种特定的符号表示形式。算法的五种性质1.有穷性2.确定性3.有效性4.输入5.输出算法设计的目标1.正确性2.可读性3.健壮性4.高效率(时间与空间)算法的描述方式:1.自然语言2.程序设计语言3.伪代码4.流程图多项式时间算法的时间复杂度有哪些形式?1.常量阶:O(1)2.线性阶:O(n)3.平方阶:O(n2)4.立方阶5.对数阶6.线性对数阶按照数据元素之间逻辑关系的特性可分为哪几类(作简要说明)?1.集合集合中数据元素之间除了“同属于一个集合”的特性外,数据元素之间无其他关系,它们之间的关系称为是松散性的2.线性结构线性结构中数据元素之间存在“一对一”的关系3.树形结构树形结构中数据元素之间存在“一对多”的关系4.图形结构图形结构中数据元素之间存在“多对多”的关系列举几个常见的链表(至少三个)?1.单链表1.循环链表2.双向链表3.双向循环链表顺序表与链表的比较1.链表比较灵活,插入和删除操作效率较高,但空间利用率低,适用于实现动态的线性表;2.顺序表实现比较简单,并且空间利用率也较高,可高效的进行随机存取,但顺序表不易扩充,插入和删除操作效率较低,适合于实现相对“稳定”的静态线性表。静态查找与动态查找分别是什么?静态查找表:查找表的操作不包含对表的修改操作。也就是仅对查找表进行查找或读表元操作。动态查找表:若在查找的同时插入了表中不存在的记录,或从查找表中删除了已存在的记录。动态表查找有什么特点?表结构本身是在查找过程中动态生成的,即对于给定值key,若表中存在关键字值等于key的记录,则查找成功返回;否则插入关键字值等于key的记录。什么是二叉排序树?二叉排序树或者是一棵空树,或者是一颗具有下列性质的二叉树:① .若左子树不空,则左子树上所有结点的值均小于根结点的值;② .若右子树不空,则右子树上所有结点的值均大于根结点的值;③ .它的左右子树也都是二叉排序树简述什么是结点的平衡因子。结点的平衡因子:该结点的左子树深度与右子树深度之差,又称为平衡度。① .平衡二叉树也就是树中任意结点的平衡因子的绝对值小于等于1的二叉树。② .在AVL树中的结点平衡因子可能有3种取值:-1、0、1在哈希表查找中,对数字的关键字常用的哈希函数有哪些(不少于5个)1. 除留余数法2. 平方取中法3. 直接定址法4. 折叠法5. 数字分析法6. 随机数法在哈希表查找中,常用的处理哈希冲突的方法有哪些(不少于3个)1. 开放定址法2. 链地址法3. 公共溢出区法4. 再哈希法computed 和 watch 的区别computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;watch: 更多的是“观察”的作用,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作;vue-router 路由模式有几种?vue-router 有 3 种路由模式:hash、history、abstract各模式的说明如下:hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.vue中为什么data是一个函数组件的data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。如果单纯的写成对象形式,就使得所有组件实例共用了一份data,这样一个实例中更新数据会导致其他实例数据改变。v-if 和 v-show 的区别v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。v-show 会被编译成指令,条件不满足时控制样式将此节点隐藏请列举几个vue内部指令,并说明其作用(至少五个)1. v-bind:绑定属性,动态更新HTML元素上的属性。例如 v-bind:class;2. v-on:用于监听DOM事件。例如 v-on:click v-on:keyup;3. v-text:更新元素的textContent;4. v-model:用来在 input、select、textarea、checkbox、radio 等表单控件元素上创建双向数据绑定,根据表单上的值,自动更新绑定的元素的值;5. v-for:循环指令编译出来的结果是 -L 代表渲染列表。优先级比v-if高最好不要一起使用,尽量使用计算属性去解决;6. v-show:使用指令来实现 -- 最终会通过display来进行显示隐藏;你建不建议v-if和v-for一起使用?为什么?v-for和v-if不要在同一标签中使用,因为解析时先解析v-for在解析v-if。如果遇到需要同时使用时可以考虑写成计算属性的方式。v-for为什么要加keyv-for遍历时,key是Vue中vnode的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。更准确是因为带 key时,在sameNode函数进行key值对比中可以避免就地复用的情况。所以会更加准确。更快速是利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快什么是微服务框架?微服务架构就是将单体的应程序分成多个应程序,这多个应程序就成为微服务,每个微服务运行在自己的进程中,并使用轻量级的通信机制。这些服务围绕业务能力来划分,并通过自动化机制来独立部署。汶些服务可以使用不同的编程语言,不同数据库,以保证最低限度的集中式管理Spring Cloud是什么Spring Cloud是一系列框架的有序集合,它利用Spring Boot的开发便利性巧妙的简化了分布式系统基础设施的开发,如服务发现、配置中心、智能路由、消息总线、负载均衡等,都可以用Spring Boot的开发风格来做到一键部署和启动。Spring Cloud将各家公司开发的比较成熟的服务框架组合起来,通过Spring Boot风格进行再封装,屏蔽掉了复杂的配置和实现原理,最终给开发者一套易懂、易部署和易维护的分布式系统开发工具包。Spring Cloud和Spring Boot的区别Spring Boot专注于快速方便的开发单个个体微服务。Spring Cloud关注全局,它将Spring Boot开发的单体微服务整合并管理起来Spring Cloud为各个微服务之间提供配置管理、服务发现、路由、熔断、降级等等集成服务。Spring Boot可以离开Spring Cloud独立使用开发项目,Spring Cloud离不开Spring Boot,属于依赖关系Spring Boot专注于微服务个体,Spring Cloud关注全局的微服务治理SpringCloud Alibaba有哪些主要功能?分布式配置:分布式系统的外部配置管理,配置中心可视化、分环境配置控制。配置动态更新能力。服务注册与发现:适配SpringCloud标准的服务注册与服务发现管理。服务限流与降级:可通过控制台进行实时的修改限流降级的规则,实时的Metrics监控。支持多种协议消息驱动:基于RocketMQ实现消息驱动的业务场景开发。分布式事务:开源Seata使用@GlobalTransactional注解,零侵入的实现分布式事务的支持。SpringCloud Alibaba核心组件有哪些?Nacos (配置中心与服务注册与发现)Sentinel (分布式流控)RocketMQ (消息队列)Seata (分布式事务)Dubbo (RPC)Nacos是什么,他有哪些关键特性?Nacos用于服务的注册发现与服务的配置管理,它提供了简单易用的Web Console。可以帮助开发者快速的实现服务发现、服务配置管理、服务元数据等需求它的关键特性主要有:服务发现和服务健康监测动态配置服务动态 DNS 服务服务及其元数据管理Sentinel是什么,它有什么作用?Sentinel是一个高可用的流量控制与防护组件,保障微服务的稳定性。Sentinel分为两个部分,sentinel-core与sentinel-dashboard。sentinel-core 部分能够支持在本地引入sentinel-core进行限流规则的整合与配置。sentinel-dashboard 则在core之上能够支持在线的流控规则与熔断规则的维护与调整等。熔断和降级的区别?服务降级有很多种降级方式!如开关降级、限流降级、熔断降级!服务熔断属于降级方式的一种!当发生下游服务不可用的情况,熔断和降级必定是一起出现。服务降级大多是属于一种业务级别的处理,熔断属于框架层级的实现什么是Feign?Feign是一个声明web服务客户端,这使得编写web服务客户端更容易Feign将我们需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要自己构建Http请求,直接调用接口即可,不过要注意的是,调用的方法要和本地抽象方法的签名完全一致。Spring Cloud Gateway是什么,它有什么作用?Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量控制组件,在微服务系统中有着非常重要的作用,网关常见的功能有转发、权限校验、限流控制等作用什么是Redis,它有哪些基本数据类型?Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库。它通过提供多种键值数据类型来适应不同场景下的存储需求,Redis支持的基本数据类型如下:字符串类型 string散列类型 hash列表类型 list集合类型 set有序集合类型 sortedset有哪些场景可以使用 RabbitMQ?服务间异步通信顺序消费定时任务请求削峰RabbitMQ有哪几种常见的工作模式?Work queuesPublish/Subscribe:发布订阅模式Routing:路由模式TopicsHeaderRPCMQ的优点有哪些?异步处理 - 相比于传统的串行、并行方式,提高了系统吞吐量。应用解耦 - 系统间通过消息通信,不用关心其他系统的处理。流量削锋 - 可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请求。日志处理 - 解决大量日志传输。消息通讯 - 消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。————————————————版权声明:本文为CSDN博主「陶然同学」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/weixin_45481821/article/details/126163199
-
释介绍redis与Spring Cache的整合请参看上章@Cacheable@Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存@Cacheable 作用和配置方法参数 解释 examplevalue 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:@Cacheable(value=”mycache”)@Cacheable(value={”cache1”,”cache2”}key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 @Cacheable(value=”testcache”,key=”#userName”)condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 @Cacheable(value=”testcache”,condition=”#userName.length()>2”)实例@Cacheable(value=”accountCache”),这个注释的意思是,当调用这个方法的时候,会从一个名叫 accountCache 的缓存中查询,如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则返回缓存中的对象。这里的缓存中的 key 就是参数 userName,value 就是 Account 对象。“accountCache”缓存是在 spring*.xml 中定义的名称。@Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache public Account getAccountByName(String userName) { // 方法内部实现不考虑缓存逻辑,直接实现业务 System.out.println("real query account."+userName); return getFromDB(userName); } @CachePut@CachePut 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用@CachePut 作用和配置方法参数 解释 examplevalue 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 @CachePut(value=”my cache”)key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 @CachePut(value=”testcache”,key=”#userName”)condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 @CachePut(value=”testcache”,condition=”#userName.length()>2”)实例@CachePut 注释,这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。@CachePut(value="accountCache",key="#account.getName()")// 更新accountCache 缓存public Account updateAccount(Account account) { return updateDB(account); } @CacheEvict@CachEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空@CacheEvict 作用和配置方法参数 解释 examplevalue 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 @CacheEvict(value=”my cache”)key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 @CacheEvict(value=”testcache”,key=”#userName”)condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 @CacheEvict(value=”testcache”,condition=”#userName.length()>2”)allEntries 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 @CachEvict(value=”testcache”,allEntries=true)beforeInvocation 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 @CachEvict(value=”testcache”,beforeInvocation=true)实例@CacheEvict(value="accountCache",key="#account.getName()")// 清空accountCache 缓存 public void updateAccount(Account account) { updateDB(account); } @CacheEvict(value="accountCache",allEntries=true)// 清空accountCache 缓存public void reload() { reloadAll()}@Cacheable(value="accountCache",condition="#userName.length() <=4")// 缓存名叫 accountCache public Account getAccountByName(String userName) { // 方法内部实现不考虑缓存逻辑,直接实现业务 return getFromDB(userName); }@CacheConfig所有的@Cacheable()里面都有一个value=“xxx”的属性,这显然如果方法多了,写起来也是挺累的,如果可以一次性声明完 那就省事了,所以,有了@CacheConfig这个配置,@CacheConfig is a class-level annotation that allows to share the cache names,如果你在你的方法写别的名字,那么依然以方法的名字为准。@CacheConfig("books")public class BookRepositoryImpl implements BookRepository { @Cacheable public Book findBook(ISBN isbn) {...}}条件缓存下面提供一些常用的条件缓存//@Cacheable将在执行方法之前( #result还拿不到返回值)判断condition,如果返回true,则查缓存; @Cacheable(value = "user", key = "#id", condition = "#id lt 10")public User conditionFindById(final Long id) //@CachePut将在执行完方法后(#result就能拿到返回值了)判断condition,如果返回true,则放入缓存; @CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'") public User conditionSave(final User user) //@CachePut将在执行完方法后(#result就能拿到返回值了)判断unless,如果返回false,则放入缓存;(即跟condition相反)@CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'")public User conditionSave2(final User user) //@CacheEvict, beforeInvocation=false表示在方法执行之后调用(#result能拿到返回值了);且判断condition,如果返回true,则移除缓存;@CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'") public User conditionDelete(final User user) @Caching有时候我们可能组合多个Cache注解使用;比如用户新增成功后,我们要添加id–>user;username—>user;email—>user的缓存;此时就需要@Caching组合多个注解标签了。@Caching(put = {@CachePut(value = "user", key = "#user.id"),@CachePut(value = "user", key = "#user.username"),@CachePut(value = "user", key = "#user.email")})public User save(User user) {自定义缓存注解比如之前的那个@Caching组合,会让方法上的注解显得整个代码比较乱,此时可以使用自定义注解把这些注解组合到一个注解中,如:@Caching(put = {@CachePut(value = "user", key = "#user.id"),@CachePut(value = "user", key = "#user.username"),@CachePut(value = "user", key = "#user.email")})@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Inheritedpublic @interface UserSaveCache {}这样我们在方法上使用如下代码即可,整个代码显得比较干净。@UserSaveCachepublic User save(User user)12扩展比如findByUsername时,不应该只放username–>user,应该连同id—>user和email—>user一起放入;这样下次如果按照id查找直接从缓存中就命中了@Caching( cacheable = { @Cacheable(value = "user", key = "#username") }, put = { @CachePut(value = "user", key = "#result.id", condition = "#result != null"), @CachePut(value = "user", key = "#result.email", condition = "#result != null") })public User findByUsername(final String username) { System.out.println("cache miss, invoke find by username, username:" + username); for (User user : users) { if (user.getUsername().equals(username)) { return user; } } return null;}其实对于:id—>user;username—->user;email—>user;更好的方式可能是:id—>user;username—>id;email—>id;保证user只存一份;如:@CachePut(value="cacheName", key="#user.username", cacheValue="#user.username") public void save(User user) @Cacheable(value="cacheName", key="#user.username", cacheValue="#caches[0].get(#caches[0].get(#username).get())") public User findByUsername(String username) SpEL上下文数据Spring Cache提供了一些供我们使用的SpEL上下文数据,下表直接摘自Spring官方文档:名称 位置 描述 示例methodName root对象 当前被调用的方法名 root.methodNamemethod root对象 当前被调用的方法 root.method.nametarget root对象 当前被调用的目标对象 root.targettargetClass root对象 当前被调用的目标对象类 root.targetClassargs root对象 当前被调用的方法的参数列表 root.args[0]caches root对象 当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”, “cache2”})),则有两个cache root.caches[0].nameargument name 执行上下文 当前被调用的方法的参数,如findById(Long id),我们可以通过#id拿到参数 user.idresult 执行上下文 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache evict’的beforeInvocation=false) result@CacheEvict(value = "user", key = "#user.id", condition = "#root.target.canCache() and #root.caches[0].get(#user.id).get().username ne #user.username", beforeInvocation = true) public void conditionUpdate(User user) ————————————————原文链接:https://blog.csdn.net/whatlookingfor/article/details/51833378/
推荐直播
-
全面解析华为云EI-API服务:理论基础与实践应用指南
2024/11/29 周五 18:20-20:20
Alex 华为云学堂技术讲师
本期直播给大家带来的是理论与实践结合的华为云EI-API的服务介绍。从“主要功能,应用场景,实践案例,调用流程”四个维度来深入解析“语音交互API,文字识别API,自然语言处理API,图像识别API及图像搜索API”五大场景下API服务,同时结合实验,来加深开发者对API服务理解。
回顾中 -
企业员工、应届毕业生、在读研究生共探项目实践
2024/12/02 周一 19:00-21:00
姚圣伟 在职软件工程师 昇腾社区优秀开发者 华为云云享专家 HCDG天津地区发起人
大神带你一键了解和掌握LeakyReLU自定义算子在ONNX网络中应用和优化技巧,在线分享如何入门,以及在工作中如何结合实际项目进行学习
即将直播 -
昇腾云服务ModelArts深度解析:理论基础与实践应用指南
2024/12/03 周二 14:30-16:30
Alex 华为云学堂技术讲师
如何快速创建和部署模型,管理全周期AI工作流呢?本期直播聚焦华为昇腾云服务ModelArts一站式AI开发平台功能介绍,同时结合基于ModelArts 的实践性实验,帮助开发者从理论到实验更好地理解和使用ModelArts。
去报名
热门标签