-
首先,跨域的域是什么?跨域的英文是:Cross-Origin。Origin 中文含义为:起源,源头,出生地。在跨域中,"域"指的是一个 Web 资源(比如网页、脚本、图片等)的源头。包括该资源的协议、主机名、端口号。在同源策略中,如果两个资源的域相同,则它们属于同一域,可以自由进行交互和共享数据。反之,如果两个资源的域不同,就会出现跨域问题。这时就需要特殊的方式来处理,如跨域资源共享(CORS)。那什么是同源策略?同源策略(Same-Origin Policy)是浏览器中的一项安全机制,用于保护用户的隐私和安全。它限制了一个网页或者脚本只能从同一个源加载的资源进行访问,而不能访问其他来源的资源。这样做可以防止恶意网站利用用户身份信息进行跨站请求伪造(CSRF)攻击,保护用户的数据安全。什么是跨站请求伪造?跨站请求伪造(CSRF,Cross-Site Request Forgery)是一种网络攻击方式。在 CSRF 攻击中,攻击者利用已认证的用户身份(例如用户在银行网站上登录后的会话信息)来伪造请求,以执行未经授权的操作。举个例子:我登录了银行网站,浏览器根据我的登录信息生成了一个会话令牌,也就是 session token。但是这个令牌被而恶意网站给拿到了,它拿着我的 token 去服务器发送请求。就可以把我银行卡里的 29 块八毛五全部转走。但是如果有同源策略的限制,恶意网站就无法直接发送请求到银行。我的 29 块八毛五就可以保住。因为恶意网站的域名与银行网站的域名不同,浏览器会阻止这种抢劫行为。什么是跨域资源共享(CORS)?为了防止被面试官笑话,这里一定要知道:跨域资源共享(CORS,Cross-Origin Resource Sharing)是一种用来解决由于浏览器的同源策略而导致的跨域请求问题的一种机制。浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。什么是简单请求?只要同时满足以下两大条件,就属于简单请求。(1)请求方法是以下三种方法之一: - HEAD - GET - POST (2)HTTP的头信息不超出以下几种字段: - Accept - Accept-Language - Content-Language - Last-Event-ID - Content-Type:只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain简单请求的工作流程如下:1. 浏览器在请求中增加一个 Origin 头部字段,其中包含当前页面的源信息(协议、主机、端口)。2. 服务器在收到这个请求后,会根据请求中的 Origin 头部信息来判断是否允许该请求。3. 如果服务器允许该请求,会在响应头部中包含一个 Access-Control-Allow-Origin 头部,"*"表示允许所有来源。4. 浏览器在收到响应后,决定是否允许页面访问该资源。什么是非简单请求?不是简单请求的,就是非简单请求。非简单请求它非简单在哪?或者说:它非简单又能怎么样?非简单请求在发起正式请求之前,会先发起一个预检请求。什么是预检请求?预检请求是用于在实际的跨域请求之前进行探测和验证,以确保服务器能够正确处理,预防跨域请求可能会引发的安全性问题。一句话就是:我去前面探探路!只有得到服务器的肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。实际 java 开发中的 CORS 解决跨域配置长这样:@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 允许所有的URL路径都可以跨域访问 registry.addMapping("/**") // 允许所有来源(即允许任何域名)的请求跨域访问 .allowedOrigins("*") // 允许发送身份验证信息(如cookies、HTTP身份验证或客户端SSL证明) .allowCredentials(true) // 允许跨域请求的HTTP方法,包括GET、POST、PUT、DELETE和OPTIONS。 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 预检请求(OPTIONS请求)的有效期,单位为秒 .maxAge(3600); } }还有别的方式可以解决跨域问题吗?有的。使用 Nginx 部署为同一域。什么意思呢?就是说 Nginx 作为唯一域,代理所有服务端,在客户端眼里,只有 Nginx 这一个域,也就不存在跨域问题,由 Nginx 拿到请求再分发给对应服务器。这里我们就不再展开。转载自https://www.cnblogs.com/cosimo/p/18023596
-
前言随着微服务架构的兴起,Spring Cloud逐渐崭露头角,成为了构建分布式系统的重要工具。对于很多初次接触Spring Cloud的开发者来说,可能不太清楚它与传统的Spring框架有何区别。本文旨在探讨Spring Cloud与Spring之间的主要差异。一、Spring框架简介Spring是一个开源的Java平台,它提供了一套全面的编程和配置模型,用于构建企业级应用程序。Spring的核心特性包括依赖注入(DI)、面向切面编程(AOP)、数据访问抽象等。通过Spring,开发者可以更加高效、简洁地开发应用程序。二、Spring Cloud简介Spring Cloud是基于Spring Boot的一套微服务工具集,它提供了一整套微服务解决方案,包括服务发现、配置管理、熔断器、负载均衡等。Spring Cloud的目标是让微服务架构的搭建变得更加简单、快速和可靠。三、Spring Cloud与Spring的主要区别关注点不同:Spring框架主要关注的是应用程序的本身,提供了一系列的基础功能,如依赖注入、事务管理等。而Spring Cloud则更加关注微服务架构中的各个组件和服务之间的通信与协作,为构建分布式系统提供了丰富的工具和解决方案。组件和服务的集成:Spring Cloud集成了许多优秀的开源项目,如Netflix的Eureka、Hystrix、Zuul等,这些组件共同构成了微服务架构的核心部分。而Spring本身并不包含这些组件,需要开发者自行集成。服务治理:Spring Cloud提供了强大的服务治理功能,包括服务发现、配置管理、熔断器、负载均衡等。这些功能使得微服务架构更加健壮、可靠和易于维护。相比之下,Spring本身并不提供这些服务治理功能。部署和扩展性:Spring Cloud的设计初衷就是为了支持快速部署和横向扩展,它允许开发者将应用程序拆分成多个独立的服务,每个服务都可以独立部署和扩展。而传统的Spring应用程序通常需要整体部署,扩展性相对较差。与Spring Boot的整合:Spring Cloud是建立在Spring Boot基础之上的,因此它充分利用了Spring Boot的优点,如自动配置、快速启动等。这使得开发者在构建微服务应用程序时,可以更加高效、简洁地开发、部署和维护。四、总结综上所述,Spring Cloud与Spring的主要区别在于它们的关注点、组件和服务的集成、服务治理、部署和扩展性以及与Spring Boot的整合。Spring Cloud作为一套微服务工具集,为构建分布式系统提供了丰富的解决方案,使得开发者可以更加轻松地应对复杂的业务需求。然而,在使用Spring Cloud时,也需要关注其复杂性、学习曲线和潜在的兼容性问题。因此,在选择使用Spring Cloud还是传统的Spring框架时,需要根据项目的具体需求和团队的实际情况进行权衡。
-
前言 在日常使用 Spring 框架进行开发的时候,对于一些板块来说,可能需要实现一个相同的功能,这个功能可以是验证你的登录信息,也可以是其他的,但是由于各个板块实现这个功能的代码逻辑都是相同的,如果一个板块一个板块进行添加的话,开发效率就会很低,所以 Spring 也想到了这点,为我们程序员提供了 SpringBoot 统一功能处理的方法实现,我们是可以直接使用的。这篇文章我将带大家一起学习 SpringBoot 统一功能的处理。 1. 拦截器 正常的判断用户是否登录的逻辑就是通过 session 来判断,对于一些网站来说,很多的功能都是需要用户进行登录之后才可以使用的,如果此时通过 session 判断出来用户处于未登录状态的话,咱们的服务器会强制用户进行登录,通过代码来显示就是这样的: //检验用户是否登录 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("userInfo") == null) { return "您未登录,请登录后再试试该功能吧"; } 我们想要在哪个功能前判断用户的登录信息,就需要在该功能的模块中添加上面这些代码,如果需要添加的功能较少还好,如果很多,那么就需要花费很多的时间,那么有人就说了:我是否可以将这些代码封装成函数,然后哪个模块需要使用只需要调用这些函数就可以了呢?可以是可以,但是使用函数封装代码还是需要在原代码的基础上调用这个函数,并且显得也不是那么优雅。那么是否有方法既不需要更改原代码,也可以使得我们写的代码很优雅呢?SpringBoot 为我们提供了一种功能——拦截器。 1.1 什么是拦截器 在 Spring Boot 中,拦截器是一种用于在处理请求之前或之后执行特定操作的组件。拦截器通常用于实现一些通用的功能,比如权限验证、日志记录等。拦截器可以拦截通过Controller的请求,并在请求处理前后执行特定的操作。 拦截器的思想正好符合我们执行其他功能之前进行身份验证验证,并且不仅在方法执行之前拦截器可以起作用,方法执行之后。我们的拦截器也可以起到作用。 1.2 拦截器的使用 拦截器的使用分为两个步骤: 定义拦截器 注册配置拦截器 1.2.1 自定义拦截器 我们自定义的拦截器需要实现 HandlerInterceptor 接口,并且重写这个接口中的方法。 package com.example.springbootbook2.interceptor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @Component @Slf4j public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("目标方法执行前执行..."); HttpSession session = request.getSession(false); if (session == null || session.getAttribute("userInfo") == null) { response.setStatus(401); return false; } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info("目标方法执行后执行..."); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("视图渲染完毕之后执行,最后执行..."); } } preHandle() 方法是在目标方法执行之前执行的,返回 true,继续执行后面的代码;返回 false,中断后续的操作 postHandle() 方法是在目标方法执行之后执行的 afterCompletion() 方法是在视图渲染之后才执行的,它还在 postHandle() 方法之后执行,并且现在因为前后端分离,我们后端基本上接触不到视图的渲染,所以这个方法使用的较少 1.2.2 注册配置拦截器 注册配置拦截器需要实现 WebMvcConfiguer 接口,并实现 addInterceptors 方法。 package com.example.springbootbook2.config; import com.example.springbootbook2.interceptor.LoginInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") //指定路径配置拦截器 .excludePathPatterns("/user/login"); //指定路径不配置拦截器 } } 在添加拦截器进行身份校验之前,我们可以直接通过对应的 url 访问到指定功能页面。 而在我们添加拦截器之后,再看是否能直接访问某些功能。 我们启动添加了拦截器之后的代码之后,点击刷新就发现不能够直接访问到某些功能了,再搭配着前端我们就可以实现强制登录的功能了。 1.3 拦截器详解 拦截器的⼊⻔程序完成之后,接下来我们来介绍拦截器的使⽤细节。拦截器的使⽤细节我们主要介绍两个部分: 拦截器的拦截路径配置 拦截器实现原理 1.3.1 拦截路径 拦截路径是指我们定义的这个拦截器,对哪些请求生效,我们在注册配置拦截器的时候,通过 addPathPatterns() 方法指定要拦截哪些 HTTP 请求,也可以通过 excludePathPatterns() 方法指定不拦截哪些请求,上面我们的 /** 表示拦截所有的 HTTP 请求,除了可以设置拦截所有请求外,还有一些其他的拦截设置: 拦截路径 含义 举例 /* ⼀级路径 能匹配/user,/book,/login,不能匹配 /user/login /** 任意级路径 能匹配/user,/user/login,/user/reg /book/* /book下的⼀级路径 能匹配/book/addBook,不能匹配/book/addBook/1,/book /book/** /book下的任意级路径 能匹配/book,/book/addBook,/book/addBook/2,不能匹配/user/login 这些拦截规则可以拦截此项⽬中的使⽤ URL,包括静态⽂件(图⽚⽂件、JS、和 CSS 等⽂件)。 知道了如何配置拦截路径之后,我们就可以解决网页上身份验证的问题了。因为 /** 会拦截所有的请求,包括前端页面请求,所以我们需要将前端页面请求排除在拦截之外。 如果我们不讲前端页面请求在外的话机会出现这种情况: 然后我们将前端页面请求不添加拦截器的话,就可以实现完整的身份校验强制登录功能了。 @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; private List<String> excludePath = Arrays.asList("/user/login", "/css/**", "/js/**", "/pic/**", "/**/*.html"); // /**/*html中的/**表示所有路径下的所有以html结尾的文件 * 表示通配符 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns(excludePath); } } 当我们未登录,然后访问其他功能的话,就会强制跳转到登录页面: 1.3.2 拦截器执行流程 正常的调用顺序是这样的: 有了拦截器之后,会在调⽤ Controller 之前进⾏相应的业务处理,执⾏的流程如下图: 当我们添加拦截器之后,在执行 Controller 方法之前,请求会先被拦截器拦截,执行 preHandle() 方法,这个方法会返回一个布尔类型的值,如果返回的是 true,那么会继续执行后面的操作;如果返回的是 false,则不会执行后面的操作 Controller 当中的方法执行完毕之后,会继续执行 postHandle() 方法以及 afterHandle() 方法,执行完毕之后,返回给浏览器响应数据。 在源码中的 DispatcherServlet 类中的 doDispatch 方法中可以看到这三个方法的执行流程: 1.3.3 适配器模式 在拦截器执行的过程中还使用了适配器模式: 适配器模式(Adapter Pattern)在计算机编程中是一种常用的设计模式,主要用于解决两个不兼容的类之间的接口匹配问题。通过将一个类的接口转换成客户端所期望的另一种接口,原本因为接口不匹配而无法一起工作的两个类现在可以一起工作。 适配器模式的优点在于它能够使原本由于接口不兼容而无法一起工作的类一起工作,提高了系统的灵活性和可扩展性。同时,它也能够减少代码的重复性,因为多个源可以共享同一个适配器。 HandlerAdapter 主要⽤于⽀持不同类型的处理器(如 Controller、HttpRequestHandler 或者 Servlet 等),让它们能够适配统⼀的请求处理流程。这样,SpringMVC可以通过⼀个统⼀的接⼝ 来处理来⾃各种处理器的请求。 本来 target 和 adaptee 是两个无法正常对接的事物,但是通过适配器 adapter,这两者之间就可以进行对接,也就类似于下面这个情况: 适配器模式⻆⾊: Target:⽬标接口(可以是抽象类或接口)客户希望直接⽤的接口 Adaptee:适配者,但是与Target不兼容 Adapter:适配器类,此模式的核⼼。通过继承或者引⽤适配者的对象,把适配者转为⽬标接⼝ client:需要使⽤适配器的对象 前⾯学习的 slf4j 就使⽤了适配器模式,slf4j 提供了⼀系列打印⽇志的 api,底层调⽤的是 log4j 或者logback 来打⽇志,我们作为调⽤者,只需要调⽤ slf4j 的 api 就⾏了。 /** * slf4j接⼝ */ interface Slf4jApi{ void log(String message); } /** * log4j 接⼝ */ class Log4j{ void log4jLog(String message){ System.out.println("Log4j打印:"+message); } } /** * slf4j和log4j适配器 */ class Slf4jLog4JAdapter implements Slf4jApi{ private Log4j log4j; public Slf4jLog4JAdapter(Log4j log4j) { this.log4j = log4j; } @Override public void log(String message) { log4j.log4jLog(message); } } /** * 客⼾端调⽤ */ public class Slf4jDemo { public static void main(String[] args) { Slf4jApi slf4jApi = new Slf4jLog4JAdapter(new Log4j()); slf4jApi.log("使⽤slf4j打印⽇志"); } } 可以看出,我们不需要改变 log4j 的api,只需要通过适配器转换下,就可以更换⽇志框架,保障系统的平稳运行。 ⼀般来说,适配器模式可以看作⼀种"补偿模式",⽤来补救设计上的缺陷。应⽤这种模式算是"⽆奈之举",如果在设计初期,我们就能协调规避接⼝不兼容的问题,就不需要使⽤适配器模式了。 所以适配器模式更多的应⽤场景主要是对正在运⾏的代码进⾏改造,并且希望可以复⽤原有代码实现新的功能.。⽐如版本升级等。 2. 统一数据返回格式 如果我们完成了一个项目的开发,但是这时候,我们突然觉得后端的各个功能的返回值返回的信息不够全面,我们需要补充一些返回信息,那么这时候就意味着所有方法的返回值都需要做出修改,这也是一个不小的工作量。这时 SpringBot 又为我们提供了解决方法——统一数据返回格式。 SpringBoot 统一数据返回格式会在各个方法进行 return 返回值之前插入一些代码逻辑,从而达到改变返回值的功能。 统一的数据返回格式使用 @ControllerAdvice 和 ResponseBodyAdvice 的方式实现。ControllerAdvice 表示控制器通知类。 我们先定义一个统一的返回类型: public enum ResultCode { SUCCESS(0), FAIL(-1), UNLOGIN(-2); //0-成功 -1 失败 -2 未登录 private int code; ResultCode(int code) { this.code = code; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } } @Data public class Result<T> { /** * 业务状态码 */ private ResultCode code; //0-成功 -1 失败 -2 未登录 /** * 错误信息 */ private String errMsg; /** * 数据 */ private T data; public static <T> Result<T> success(T data){ Result result = new Result(); result.setCode(ResultCode.SUCCESS); result.setErrMsg(""); result.setData(data); return result; } public static <T> Result<T> fail(String errMsg){ Result result = new Result(); result.setCode(ResultCode.FAIL); result.setErrMsg(errMsg); result.setData(null); return result; } public static <T> Result<T> fail(String errMsg,Object data){ Result result = new Result(); result.setCode(ResultCode.FAIL); result.setErrMsg(errMsg); result.setData(data); return result; } public static <T> Result<T> unlogin(){ Result result = new Result(); result.setCode(ResultCode.UNLOGIN); result.setErrMsg("用户未登录"); result.setData(null); return result; } } package com.example.springbootbook2.config; import com.example.springbootbook2.model.Result; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; @ControllerAdvice public class ResponseAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter returnType, Class converterType) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { return Result.success(body); } } 继承 ResponseBodyAdvice 接口后,需要实现该接口下的 supports 方法和 beforeBodyWrite 方法,supports 方法只需要更改返回值为 true 就可以了,表示是否要执行 beforeBodyWrite 方法,返回 true 表示执行,false 表示不执行,beforeBodyWrite 方法中的 body 参数就是我们原方法的返回值。 当我们关闭拦截器,访问图书列表页之后,就发现我们的返回类型发生了变化: 但是这个统一数据返回格式也存在问题,当我们的返回值为 String 类型的话机会出现错误: 这是为什么呢?SpringMVC默认会注册⼀些⾃带的 HttpMessageConverter (从先后顺序排列分别为 ByteArrayHttpMessageConverter ,StringHttpMessageConverter , SourceHttpMessageConverter ,AllEncompassingFormHttpMessageConverter ) 其中 AllEncompassingFormHttpMessageConverter 会根据项⽬依赖情况添加对应的 HttpMessageConverter 在依赖中引⼊jackson包后,容器会把 MappingJackson2HttpMessageConverter ⾃动注册到 messageConverters 链的末尾。Spring会根据返回的数据类型,从 messageConverters 链选择合适的 HttpMessageConverter。当返回的数据是⾮字符串时,使⽤的MappingJackson2HttpMessageConverter 写⼊返回对象。当返回的数据是字符串时, StringHttpMessageConverter 会先被遍历到,这时会认为 StringHttpMessageConverter 可以使用。 然⽽⼦类 StringHttpMessageConverter 的addDefaultHeaders⽅法定义接收参数为String,此 时t为Result类型,所以出现类型不匹配"Result cannot be cast to java.lang.String"的异常。 那么如何解决返回类型为 String 类型的问题呢?如果返回结果为String类型, 使⽤SpringBoot内置提供的Jackson来实现信息的序列化。 @ControllerAdvice public class ResponseAdvice implements ResponseBodyAdvice { @Autowired private ObjectMapper objectMapper; @Override public boolean supports(MethodParameter returnType, Class converterType) { return true; } @SneakyThrows @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof String) { return objectMapper.writeValueAsString(Result.success(body)); }else if (body instanceof Result) { return body; }else { return Result.success(body); } } } @SneakyThrows 的主要目的是解决 Java 的异常处理问题。当我们在代码中抛出一个异常时,如果这个异常被包裹在一个方法中,并且这个方法没有 throws 关键字来声明会抛出这个异常,那么编译器会报错。通过使用 @SneakyThrows,你可以告诉编译器:“我知道这个方法可能会抛出异常,但我保证在 catch 块中处理它。” 这样编译器就不会报错了。 通过这个处理,当返回的数据类型为 String 的时候就不会出现错误了。 统一数据返回格式的优点: ⽅便前端程序员更好的接收和解析后端数据接口返回的数据 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,因为所有接口都是这样返回的 有利于项目统⼀数据的维护和修改 有利于后端技术部⻔的统⼀规范的标准制定,不会出现稀奇古怪的返回内容 3. 统一异常处理 当我们的程序中出现异常的时候,我们需要解决这些异常,因为前面做了统一数据返回格式的处理,所以这里的异常也可以进行统一的处理。 统⼀异常处理使⽤的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表⽰控制器通知类, @ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执⾏某个⽅法事件。 package com.example.springbootbook2.config; import com.example.springbootbook2.model.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; @ControllerAdvice @Slf4j @ResponseBody //因为返回的数据都不是视图类型,所以加上这个注解防止出现问题 public class ErrorHandler { @ExceptionHandler public Result<String> exception(Exception e) { log.error("发生异常:e{}",e); return Result.fail("内部异常"); } @ExceptionHandler public Result<String> exception(NullPointerException e) { log.error("发生异常:e{}",e); return Result.fail("NullPointerException异常,请联系管理员"); } @ExceptionHandler public Result<String> exception(ArithmeticException e) { log.error("发生异常:e{}",e); return Result.fail("ArithmeticException异常,请联系管理员"); } } @Controller @RequestMapping("/test") public class TestController { @RequestMapping("/t1") public Integer test1() { return 10/0; } } 这个 Exception 和 ArithmeticException 的先后顺序可以不考虑,这个不会因为 Exception 在前面捕获就报的 Exception 异常,而是会根据自己出现的最接近的异常来捕获。 ———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/m0_73888323/article/details/135587096
-
前言 在上一篇文章中,讲诉了SpringIoC的Bean装配,但是对于如何进行获取,也就是Bean之间的依赖还未讲诉,下面开始讲诉依赖注入(Dependency Injection,DI)以及如何使用属性文件。涉及主要注解@Autowired、@Primary、@Quelifier、@PropertySource和@ConfigurationProperties。 一、🌕依赖注入 例:人类(Person)有时候利用一些动物(Animal)去完成一些事情,比方说狗(Dog)是用来看门的,猫(Cat)是用来抓老鼠的, 鹦鹉(Paηot)是用来迎客的……于是做一些事情就依赖于那些可爱的动物。假设现在需要用狗狗来看门。 //定义人类接口 public interface Person { void service(); void setAnimal(Animal animal); } //定义动物接口 public interface Animal { void user(); } //定义狗 @Component public class Dog implements Animal { @Override public void user() { System.out.println("狗【" + Dog.class.getSimpleName() + "】是用来看门的"); } } //定义年轻人 @Component public class YoungPerson implements Person { @Autowired private Animal animal = null; @Override public void service() { this.animal.user(); } @Override public void setAnimal(Animal animal) { this.animal = animal; } } //定义配置类 @Configuration @ComponentScan("com.dragon.restart")//所有的包和类都在restart下 public class AppConfig { } 测试类: ApplicationContext ctx =new AnnotationConfigApplicationContext(AppConfig.class) ; Person person= ctx.getBean(YoungPerson.class) ; person.service() ; 测试是成功的,这个时候SpringIoC 容器己经通过注解@Autowired成功地将Dog注入到了 YoungPerson 实例中。 但是这只是一个比较简单的例子,我们有必要继续探讨@Autowired。 🌖注解@Autowired @Autowired 是我们使用得最多的注解之一, 因此在这里需要进一步地探讨它。 它注入的机制最基本的一条是根据类型 (bytype), 我们回顾IoC 容器的顶级接口 BeanFactory,就可以知道 IoC 容器是通过getBean 方法获取对应Bean 的,而 getBean 又支持根据类型(by type)或者根据名称(by name)。这里明显根据类型注入,将狗狗实例注入Animal对象。 再回到上面的例子,我们只是创建了一个动物一一狗,而实际上动物还可以有猫 (Cat),猫可以为我们抓老鼠, 于是我们又创建了一个猫的类。 @Component public class Cat implements Animal{ @Override public void user() { System.out.println("猫【" + Cat.class.getSimpleName() + "】是抓老鼠的"); } } 好了,如果我们还使用着YoungPerson类,那么麻烦来了,因为这个类只是定义了一个动物属性(Animal),而我们却有两个动物,一个狗, 一个猫, SpringIoC 如何注入呢? 运行测试,可以看到IoC容器抛出异常,如下: Description: Field animal in com.dragon.restart.pojo.impl.YoungPerson required a single bean, but 2 were found: - cat: defined in file [E:\IDEA_projects\restart\target\classes\com\dragon\restart\pojo\Cat.class] - dog: defined in file [E:\IDEA_projects\restart\target\classes\com\dragon\restart\pojo\impl\Dog.class] 那么使用@Autowired 能处理这个问题吗?答案是肯定的。假设我们目前需要的是狗提供服务,那么可以把属性名称转化为dog,也就是原来YoungPerson的private Animal animal = null; 改为private An工mal dog = null; @Component public class YoungPerson implements Person { @Autowired private Animal dog = null; @Override public void service() { this.dog.user(); } @Override public void setAnimal(Animal animal) { this.dog = animal; } } 这里, 我们只是将属性的名称从animal 修改为了 dog,那么我们再测试的时候,你可以看到是采用狗来提供服务的。那是因为@Autowired提供这样的规则:首先它会根据类型找到对应的Bean,如果对应类型的 Bean 不是唯一的,那么它会根据其属性名称和 Bean 的名称进行匹配。如果匹配得上,就会使用该Bean:如果还无法匹配,就会抛出异常。这里还要注意的是@Autowired 是一个默认必须找到对应 Bean 的注解,如果不能确定其标注属性一定会存在并且允许这个被标注的属性为null, 那么你可以配置@Autowired属性 required 为 false如@Autowired(required = false) @Autowired除了可以标注属性外,还可以标注方法, 如setAnimal方法,如下所示: @Override @Autowired public void setAnimal (Animal animal) { this.animal = animal; 这样它也会使用setAnimal 方法从 IoC 容器中找到对应的动物进行注入,甚至我们还可以使用在方法的参数上,后续文章会再谈到它。 🌗消除歧义性——@Quelifier和@Primary 在上面我们发现有猫有狗的时候, 为了使@Autowired 能够继续使用,我们做了一个决定,将YoungPerson 的属性名称从 animal 修改为 dog。显然这是一个憋屈的做法,好好的一个动物,却被我们定义为了狗,毕竟不能每次换个对象就改一次,这样太麻烦了。产生注入失败的问题根本是按类型(bytype) 查找, 正如动物可以有多种类型,这样会造成 Spring IoC 容器注入的困扰,我们把这样的一个问题称为歧义性。知道这个原因后, 那么这两个注解是从哪个角度去解决这些问题的呢? 首先是一个注解@Primary,它是一个修改优先权的注解,当我们有猫有狗的时候,假设这次需要使用猫, 那么只需要在猫类的定义上加入@Primarγ就可以了,如下: @Component @Primary public class Dog implements Animal { @Override public void user() { System.out.println("狗【" + Dog.class.getSimpleName() + "】是用来看门的"); } } 这里的@Primary 的含义告诉 Spring IoC 容器, 当发现有多个同样类型的 Bean 时,请优先使用我进行注入,于是再进行测试时会发现,系统将用狗狗为你提供服务。 因为当 Spring 进行注入的时候,虽然它发现存在多个动物, 但因为Dog被标注为了@Primarγ,所以优先采用Dog的实例进行了注入,这样就通过优先级的变换使得IoC容器知道注入哪个具体的实例来满足依赖注入。然后,有时候@Primary 也可以使用在多个类上,也许无论是猫还是狗狗都可能带上@Primary 注解, 其结果是IoC容器还是无法区分采用哪个Bean的实例进行注入, 又或者说我们需要更加灵活的机制来实现注入,那么**@Quelifier** 可以满足你的这个愿望。 它将与@Autowired 组合在一起,通过类型和名称一起找到Bean。我们知道Bean 名称在 Spring IoC 容器中是唯一的标识,通过这个就可以消除歧义性了。此时你是否想起了BeanFactory接口中的这个方法呢?<T> T getBean(String name, Class<T> requiredType) throws BeansException; 代码: @Component public class YoungPerson implements Person { @Autowired @Qualifier("dog") private Animal animal = null; @Override public void service() { this.animal.user(); } @Override public void setAnimal(Animal animal) { this.animal = animal; } } 一旦这样声明, Spring IoC 将会以类型和名称去寻找对应的Bean进行注入。根据类型和名称,显然也只能找到狗狗为我们服务了。 🌑带有参数的构造方法类装配 在上面,我们都基于一个默认的情况,那就是不带参数的构造方法下实现依赖注入。但事实上,有些类只有带有参数的构造方法,于是上述的方法都不能再使用了。为了满足这个功能,我们可以使用@Autowired 注解对构造方法的参数进行注入,例如,修改类YoungPerson来满足这个功能。 @Component public class YoungPerson implements Person { private Animal animal = null; public YoungPerson(@Autowired @Qualifier("dog") Animal animal) { this.animal = animal; } @Override public void service() { this.animal.user(); } @Override public void setAnimal(Animal animal) { this.animal = animal; } } @Autowired 和@Qualifier 注解,使其注入进来。这里使用@Qualifier 是为了避免歧义性。当然如果你的环境中不是有猫有狗,则可以完全不使用@Qualifier,而单单使用@Autowired就可以了。 二、📝使用属性文件 Java 开发使用属性文件已经十分普遍,所以这里谈谈这方面的内容。在Spring Boot 中使用属性文件,可以采用其默认为我们准备的application.properties,也可以使用自定义的配置文件。 应该说读取配置文件的方法很多, 这里没有必要面面俱到地介绍每一个细节,只是介绍那些最常用的方法。 在SpringBoot项目的application.properties: database.driverName=com.mysql.jdbc.Driver database.url=jdbc:mysql://localhost:3306/my database.username=root database.password=root 创建个DataBaseProperties类: /** * @Version: 1.0.0 * @Author: Dragon_王 * @ClassName: DataBaseProperties * @Description: TODO描述 * @Date: 2024/1/16 16:07 */ @Component public class DataBaseProperties { @Value("${database.driverName}") private String driverName = null; @Value("${database.url}") private String url = null; private String username = null; private String password = null; public void setDriverName(String driverName) { System.out.println(driverName); this.driverName = driverName; } public void setUrl(String url) { System.out.println(url); this.url = url; } @Value("${database.username}") public void setUsername(String username) { System.out.println(username); this.username = username; } @Value("${database.password}") public void setPassword(String password) { System.out.println(password); this.password = password; } public String getDriverName() { return driverName; } public String getUrl() { return url; } public String getUsername() { return username; } public String getPassword() { return password; } } 通过@Value注解, 使用${… }这样的占位符读取配置在属性文件的内容。这里的@Value 注解,既可以加载属性, 也可以加在方法上。 这样就能成功将属性文件内容成功注入了。但是我们可以使用过注解@ConfigurationProperties来简化一下,同样能实现注入,如下: /** * @Version: 1.0.0 * @Author: Dragon_王 * @ClassName: DataBaseProperties * @Description: TODO描述 * @Date: 2024/1/16 16:07 */ @Component @ConfigurationProperties("database") public class DataBaseProperties { private String driverName = null; private String url = null; private String username = null; private String password = null; public void setDriverName(String driverName) { System.out.println(driverName); this.driverName = driverName; } public void setUrl(String url) { System.out.println(url); this.url = url; } public void setUsername(String username) { System.out.println(username); this.username = username; } public void setPassword(String password) { System.out.println(password); this.password = password; } public String getDriverName() { return driverName; } public String getUrl() { return url; } public String getUsername() { return username; } public String getPassword() { return password; } } 这里@ConfigurationProperties注解内的database将会与属性名组成属性的全限定名去配置文件里查找,如属性driveName就会和database组成database.driverName去查找,这里注意一下不需要管字母的大小写,不影响。 如果我不用默认的application.properties文件怎么办呢?现在我们重新创建一个jdbc.properties文件,将原配置文件内容移入其中。 DataBaseProperties 类不变如上,启动类配置如下: @SpringBootApplication @PropertySource(value = {"classpath:jdbc.properties"},ignoreResourceNotFound = true) public class RestartApplication { public static void main(String[] args) { SpringApplication.run(RestartApplication.class, args); } } value 可以配置多个配置文件。使用 classpath 前缀, 意味着去类文件路径下找到属性文件;ignoreResourceNotFound 则是是否忽略配置文件找不到的问题。 ignoreResourceNotFound 的默认值为false,也就是没有找到属性文件, 就会报错;这里配置为true,也就是找不到就忽略掉,不会报错。通过运行日志,可以看出成功注入。 ———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/weixin_62951900/article/details/135626611
-
系统介绍: 本论文主要论述了如何使用JAVA语言开发一个音乐网站与分享平台 ,本系统将严格按照软件开发流程进行各个阶段的工作,采用B/S架构,面向对象编程思想进行项目开发。在引言中,作者将论述音乐网站与分享平台的当前背景以及系统开发的目的,后续章节将严格按照软件开发流程,对系统进行各个阶段分析设计。 音乐网站与分享平台的主要使用者分为管理员和用户,实现功能包括管理员:首页、个人中心、用户管理、音乐资讯管理、音乐翻唱管理、在线听歌管理、留言板管理、系统管理,用户:首页、个人中心、音乐翻唱管理、我的收藏管理,前台首页;首页、音乐资讯、音乐翻唱、在线听歌、留言反馈、个人中心、后台管理、客服等功能。由于本网站的功能模块设计比较全面,所以使得整个音乐网站与分享平台信息管理的过程得以实现。 本系统的使用可以实现本音乐网站与分享平台管理的信息化,可以方便管理员进行更加方便快捷的管理,可以提高管理人员的工作效率。 关键词:音乐网站与分享平台 JAVA语言;MYSQL数据库;Spring Boot框架 整个系统是由多个功能模块组合而成的,要将所有的功能模块都一一列举出来,然后进行逐个的功能设计,使得每一个模块都有相对应的功能设计,然后进行系统整体的设计。 本音乐网站与分享平台结构图如图4-2所示。 图片请到原文查看!!!图4-2 系统功能结构图 程序上交给用户进行使用时,需要提供程序的操作流程图,这样便于用户容易理解程序的具体工作步骤,现如今程序的操作流程都有一个大致的标准,即先通过登录页面提交登录数据,通过程序验证正确之后,用户才能在程序功能操作区页面操作对应的功能。 程序操作流程图 功能截图: 5.1前台首页功能模块 音乐网站与分享平台 ,在系统首页可以查看首页、音乐资讯、音乐翻唱、在线听歌、留言反馈、个人中心、后台管理、客服等内容,如图5-1所示。 图5-1系统功能界面图 登录、用户注册,在用户注册页面可以填写用户名、密码、姓名、年龄、手机、邮箱、身份证等信息进行注册,如图5-2所示。 图5-2登录、用户注册界面图 在线听歌,在在线听歌页面通过填写编号、歌曲名称、标签、原唱、作曲、作词、音乐、视频等信息进行点我收藏,如图5-3所示。在音乐翻唱页面通过填写编号、歌曲名称、原唱、标签、音频、图片、翻唱原因、用户名等信息进行点我收藏操作,如图5-4所示。 图5-3在线听歌界面图 图5-4音乐翻唱界面图 5.2管理员功能模块 管理员登录,通过填写注册时输入的用户名、密码、角色进行登录,如图5-5所示。 图5-5管理员登录界面图 管理员登录进入音乐网站与分享平台可以查看首页、个人中心、用户管理、音乐资讯管理、音乐翻唱管理、在线听歌管理、留言板管理、系统管理等信息。 个人信息,在个人信息页面中可以通过填写用户名等内容进行修改、添加,如图5-6所示。还可以根据需要对用户管理进行添加,修改或删除等详细操作,如图5-7所示。 图5-6个人信息界面图 图5-7用户管理界面图 音乐资讯管理,在音乐资讯管理页面中可以填写标题、资讯类型、图片、摘要 等信息,并可根据需要对已有音乐资讯管理进行修改或删除等操作,如图5-8所示。 图5-8音乐资讯管理界面图 音乐翻唱管理,在音乐翻唱管理页面中可以填写编号、歌曲名称、原唱、标签、音频、图片、翻唱原因、用户名等信息,并可根据需要对已有音乐翻唱管理进行修改或删除等详细操作,如图5-9所示。 图5-9音乐翻唱管理界面图 在线听歌管理,在在线听歌管理页面中可以查看编号、歌曲名称、标签、原唱、作曲、作词、音乐、视频、图片等内容,并且根据需要对已有在线听歌管理进行添加,修改或删除等详细操作,如图5-10所示。 图5-10在线听歌管理界面图 留言板管理,在留言板管理页面中可以查看用户名、留言内容、回复内容等内容,并且根据需要对已有留言板管理进行添加,修改或删除等详细操作,如图5-11所示。 图5-11留言板管理界面图 客服管理,在客服管理页面中可以查看新消息、状态等内容,并且根据需要对已有客服管理进行添加,修改或删除等详细操作,如图5-12所示。 图5-12客服管理界面图 轮播图;该页面为轮播图管理界面。管理员可以在此页面进行首页轮播图的管理,通过新建操作可在轮播图中加入新的图片,还可以对以上传的图片进行修改操作,以及图片的删除操作,如图5-13所示。 图5-13轮播图管理界面图 5.3用户功能模块 用户登录进入音乐网站与分享平台可以查看首页、个人中心、音乐翻唱管理、我的收藏管理等内容。 个人中心,在个人中心页面中通过填写原密码、新密码、确认密码等信息,还可以根据需要对个人中心进行修改、删除如图5-14所示。 图5-14个人中心界面图 个人信息,在个人信息页面中可以查看用户名、密码、姓名、年龄、性别、手机、邮箱、身份证等信息内容,并且根据需要对已有个人信息进行修改或删除等其他详细操作,如图5-15所示。 图5-15个人信息界面图 音乐翻唱管理,在音乐翻唱管理页面中通过填写编号、歌曲名称、原唱、标签、音频、图片、翻唱原因、用户名等内容进行查看、删除,如图5-16所示。 图5-16音乐翻唱管理界面图 我的收藏管理,在我的收藏管理页面中可以查看收藏ID、表名、收藏名称、收藏图片等内容,并且根据需要对已有我的收藏管理进行查看等详细操作,如图5-17所示。 图5-17我的收藏管理界面图 代码实现: /** * 登录相关 */ @RequestMapping("users") @RestController public class UserController{ @Autowired private UserService userService; @Autowired private TokenService tokenService; /** * 登录 */ @IgnoreAuth @PostMapping(value = "/login") public R login(String username, String password, String role, HttpServletRequest request) { UserEntity user = userService.selectOne(new EntityWrapper<UserEntity>().eq("username", username)); if(user != null){ if(!user.getRole().equals(role)){ return R.error("权限不正常"); } if(user==null || !user.getPassword().equals(password)) { return R.error("账号或密码不正确"); } String token = tokenService.generateToken(user.getId(),username, "users", user.getRole()); return R.ok().put("token", token); }else{ return R.error("账号或密码或权限不对"); } } /** * 注册 */ @IgnoreAuth @PostMapping(value = "/register") public R register(@RequestBody UserEntity user){ // ValidatorUtils.validateEntity(user); if(userService.selectOne(new EntityWrapper<UserEntity>().eq("username", user.getUsername())) !=null) { return R.error("用户已存在"); } userService.insert(user); return R.ok(); } /** * 退出 */ @GetMapping(value = "logout") public R logout(HttpServletRequest request) { request.getSession().invalidate(); return R.ok("退出成功"); } /** * 密码重置 */ @IgnoreAuth @RequestMapping(value = "/resetPass") public R resetPass(String username, HttpServletRequest request){ UserEntity user = userService.selectOne(new EntityWrapper<UserEntity>().eq("username", username)); if(user==null) { return R.error("账号不存在"); } user.setPassword("123456"); userService.update(user,null); return R.ok("密码已重置为:123456"); } /** * 列表 */ @RequestMapping("/page") public R page(@RequestParam Map<String, Object> params,UserEntity user){ EntityWrapper<UserEntity> ew = new EntityWrapper<UserEntity>(); PageUtils page = userService.queryPage(params, MPUtil.sort(MPUtil.between(MPUtil.allLike(ew, user), params), params)); return R.ok().put("data", page); } /** * 信息 */ @RequestMapping("/info/{id}") public R info(@PathVariable("id") String id){ UserEntity user = userService.selectById(id); return R.ok().put("data", user); } /** * 获取用户的session用户信息 */ @RequestMapping("/session") public R getCurrUser(HttpServletRequest request){ Integer id = (Integer)request.getSession().getAttribute("userId"); UserEntity user = userService.selectById(id); return R.ok().put("data", user); } /** * 保存 */ @PostMapping("/save") public R save(@RequestBody UserEntity user){ // ValidatorUtils.validateEntity(user); if(userService.selectOne(new EntityWrapper<UserEntity>().eq("username", user.getUsername())) !=null) { return R.error("用户已存在"); } userService.insert(user); return R.ok(); } /** * 修改 */ @RequestMapping("/update") public R update(@RequestBody UserEntity user){ // ValidatorUtils.validateEntity(user); userService.updateById(user);//全部更新 return R.ok(); } /** * 删除 */ @RequestMapping("/delete") public R delete(@RequestBody Integer[] ids){ userService.deleteBatchIds(Arrays.asList(ids)); return R.ok(); } } 论文参考: 推荐项目: 基于微信小程序+Springboot线上租房平台设计和实现-三端 2022-2024年最全的计算机软件毕业设计选题大全 基于Java+SpringBoot+Vue前后端分离手机销售商城系统设计和实现 基于Java+SpringBoot+Vue前后端分离仓库管理系统设计实现 基于SpringBoot+uniapp微信小程序校园点餐平台详细设计和实现 基于Java+SpringBoot+Vue+echarts健身房管理系统设计和实现 基于JavaSpringBoot+Vue+uniapp微信小程序实现鲜花商城购物系统 基于Java+SpringBoot+Vue前后端分离摄影分享网站平台系统 基于Java+SpringBoot+Vue前后端分离餐厅点餐管理系统设计和实现 基于Python热门旅游景点数据分析系统设计与实现 源码获取: 大家点赞、收藏、关注、评论啦 、查看👇🏻获取联系方式👇🏻 精彩专栏推荐订阅:在下方专栏👇🏻 2022-2024年最全的计算机软件毕业设计选题大全:1000个热门选题推荐✅ Java项目精品实战案例《100套》 Java微信小程序项目实战《100套》 Python项目实战《100套》 ———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/weixin_39709134/article/details/135640044
-
强烈推荐 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站:人工智能 前言 在Java开发领域,Spring框架一直以来都是一个强大而广泛使用的工具,用于构建企业级应用。 然而,随着时间的推移和技术的发展,Spring Boot应运而生,为Java开发者带来了一种更为简化、快速的开发体验。 本文将深入探讨Spring和Spring Boot之间的区别,着重于它们在项目配置、依赖管理、内嵌服务器、开发体验等方面的差异。通过比较这两个框架,我们旨在帮助开发者更好地理解它们的特性,以便在实际项目中做出明智的选择。 区别 项目配置: Spring: 在Spring中,项目配置通常需要使用XML文件(如applicationContext.xml)或Java配置类来定义Bean、注入依赖等。 Spring Boot: Spring Boot采用约定大于配置的原则,通过默认的配置减少了开发者的工作量。它使用注解和自动配置,不需要繁琐的XML配置,大多数配置可以通过属性文件(如application.properties或application.yml)进行管理。 依赖管理: Spring: 在Spring中,你需要手动管理项目的各种依赖,包括版本控制。 Spring Boot: Spring Boot使用了“Starter”依赖,这是一组预定义好的依赖集,可以快速集成常用的功能,开发者只需要在项目中引入相关的Starter依赖,而不需要手动配置每个依赖的版本。 内嵌服务器: Spring: 在Spring中,你需要手动配置和集成Web服务器,如Tomcat或Jetty。 Spring Boot: Spring Boot内置了嵌入式的Web服务器(如Tomcat、Jetty或Undertow),默认情况下不需要任何额外的配置,你可以直接运行一个独立的JAR文件来启动应用。 开发体验: Spring: 需要手动配置和管理很多细节,可能显得繁琐。 Spring Boot: 简化了配置和开发流程,使开发者更专注于业务逻辑的实现,提供了更好的开发体验。 实例 下面通过一个简单的示例来比较Spring和Spring Boot的应用。 假设我们要创建一个简单的RESTful Web服务,提供关于用户的基本信息。 Spring项目示例: 创建一个Maven项目: 在Maven项目中,需要手动添加依赖,配置web.xml等。 定义一个用户实体类: public class User { // 属性、构造函数、getter和setter等 } 1 2 3 创建一个Controller: @Controller public class UserController { @Autowired private UserService userService; @GetMapping("/users/{id}") public ResponseEntity<User> getUserById(@PathVariable Long id) { User user = userService.getUserById(id); return new ResponseEntity<>(user, HttpStatus.OK); } } 1 2 3 4 5 6 7 8 9 10 11 配置XML文件: 配置Spring的XML文件,定义Bean,设置数据库连接等。 Spring Boot项目示例: 创建一个Spring Boot项目: 使用Spring Initializer或者Spring Boot CLI创建一个项目,选择相关依赖。 定义一个用户实体类: @Entity public class User { // 属性、构造函数、getter和setter等 } 1 2 3 4 创建一个Controller: @RestController public class UserController { @Autowired private UserService userService; @GetMapping("/users/{id}") public ResponseEntity<User> getUserById(@PathVariable Long id) { User user = userService.getUserById(id); return new ResponseEntity<>(user, HttpStatus.OK); } } 1 2 3 4 5 6 7 8 9 10 11 配置文件: 使用application.properties或application.yml配置数据库连接等信息。 通过简单的例子可以看到在Spring Boot中,项目结构更简洁,依赖管理更方便,而且不需要过多的XML配置。 Spring Boot通过默认值和自动配置减少了开发者的工作,提供了更好的开发体验,特别是在快速构建和部署微服务等场景中。 总结 本文中,我们详细讨论了Spring和Spring Boot的差异,从传统的Spring框架演进到更为现代、轻量级的Spring Boot。 Spring Boot以其自动化配置、内嵌服务器和简化的开发流程而脱颖而出,为开发者提供了更为便捷的工具,特别适用于构建微服务和快速原型开发。 然而,这并不意味着Spring框架失去了其价值,它仍然是一个强大的选择,特别是在需要更细粒度控制和传统项目迁移的情况下。 选择使用Spring还是Spring Boot,取决于项目的需求、开发者的偏好以及特定场景下的最佳实践。无论选择哪个框架,都是为了更好地服务于Java应用程序的开发,满足不同项目的需求。 强烈推荐 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站:人工智能 专栏集锦 大佬们可以收藏以备不时之需: Spring Boot 专栏:http://t.csdnimg.cn/peKde ChatGPT 专栏:http://t.csdnimg.cn/cU0na Java 专栏:http://t.csdnimg.cn/YUz5e Go 专栏:http://t.csdnimg.cn/Jfryo Netty 专栏:http://t.csdnimg.cn/0Mp1H Redis 专栏:http://t.csdnimg.cn/JuTue Mysql 专栏:http://t.csdnimg.cn/p1zU9 架构之路 专栏:http://t.csdnimg.cn/bXAPS 写在最后 感谢您的支持和鼓励! 😊🙏 如果大家对相关文章感兴趣,可以关注公众号"架构殿堂",会持续更新AIGC,java基础面试题, netty, spring boot, spring cloud等系列文章,一系列干货随时送达! ———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/jinxinxin1314/article/details/135964734
-
1 前言我一年java,在小公司,当前公司权限这块都没有成熟的方案,目前我知道权限分为功能权限和数据权限,我不知道数据权限这块大家是怎么解决的,但在实际项目中我遇到数据权限真的复杂,你永远不知道业主在这方面的需求是什么。我也有去搜索在这方面是怎么做,但是我在gitee、github搜到的权限管理系统他们都是这么实现的:查看全部数据、自定义数据权限、本部门数据权限、本部门及以下数据、仅本人数据权限,但是这种控制粒度完全不够的,所以就想自己实现一下。2 需求需求一有一个单位和企业的树,企业都是挂在某个单位下面的,企业是分类型的(餐饮企业、经营企业、生产企业),业主需要单位的人限定某些单位只能看一个或他指定的某个类型的企业。现在指定角色A只能查看餐饮、经营的企业,那就只能使用查看自定义部门数据这个,然后在10000家企业里面慢慢勾选符合的企业,这样可以是可以,但是我觉得这样做不太妥。估计有人说:那你把三种类型的企业分组,餐饮企业挂在餐饮分组下,其他同理。然后用自定义数据权限选中那两个不就可以了吗?可以是可以,但是我不是业主,业主要求了那些企业必须挂在哪些单位下,在页面显示的树也不能显示什么餐饮企业分组、生产企业... 说到底,除非你有办法改变业主的想法。需求二类似订单吧,角色A只能查看未支付的订单,角色B只能看交易金额在100~1000元的订单。用通用的那5种权限对这两个需求已经是束手无策了。3 设计思路后来我看到一篇文章【数据权限就该这么实现(设计篇)[1]】,对我有很大的启发,从数据库字段下手,用规则来处理我以这个文章的思路为基础,设计了这么一个关系主要还是这张规则表,通过在页面配置好相关的规则来实现对某个字段的控制CREATETABLE`sys_rule`( `id`bigintNOTNULL, `remark`varchar(255)CHARACTERSETutf8mb3COLLATEutf8mb3_general_ciDEFAULTNULLCOMMENT'备注', `mark_id`bigintDEFAULTNULL, `table_alias`varchar(255)CHARACTERSETutf8mb3COLLATEutf8mb3_general_ciDEFAULTNULLCOMMENT'表别名', `column_name`varchar(255)CHARACTERSETutf8mb3COLLATEutf8mb3_general_ciDEFAULTNULLCOMMENT'数据库字段名', `splice_type`varchar(255)CHARACTERSETutf8mb3COLLATEutf8mb3_general_ciDEFAULTNULLCOMMENT'拼接类型SpliceTypeEnum', `expression`varchar(255)CHARACTERSETutf8mb3COLLATEutf8mb3_general_ciDEFAULTNULLCOMMENT'表达式ExpressionEnum', `provide_type`tinyintDEFAULTNULLCOMMENT'ProvideTypeEnum值提供类型,1-值,2-方法', `value1`varchar(255)CHARACTERSETutf8mb3COLLATEutf8mb3_general_ciDEFAULTNULLCOMMENT'值1', `value2`varchar(255)CHARACTERSETutf8mb3COLLATEutf8mb3_general_ciDEFAULTNULLCOMMENT'值2', `class_name`varchar(255)CHARACTERSETutf8mb3COLLATEutf8mb3_general_ciDEFAULTNULLCOMMENT'全限定类名', `method_name`varchar(255)CHARACTERSETutf8mb3COLLATEutf8mb3_general_ciDEFAULTNULLCOMMENT'方法名', `formal_param`varchar(255)CHARACTERSETutf8mb3COLLATEutf8mb3_general_ciDEFAULTNULLCOMMENT'形参,分号隔开', `actual_param`varchar(255)CHARACTERSETutf8mb3COLLATEutf8mb3_general_ciDEFAULTNULLCOMMENT'实参,分号隔开', `create_time`datetimeDEFAULTNULL, `create_by`bigintDEFAULTNULL, `update_time`datetimeDEFAULTNULL, `update_by`bigintDEFAULTNULL, `deleted`bit(1)DEFAULTNULL, PRIMARYKEY(`id`)USINGBTREE )ENGINE=InnoDBDEFAULTCHARSET=utf8mb3ROW_FORMAT=DYNAMICCOMMENT='规则表';整体思路就是通过页面来对特定的接口设置规则,如果提供类型是值且@DataScope注解用在方法上,那么默认机会在执行SQL前去拼接对应的数据权限。如果提供类型是方法且@DataScope注解用在方法上,那么会根据你配置的方法名、参数类型去反射执行对应的方法,得到该规则能查看的所有idList,然后在执行SQL前去拼接对应的数据权限,这是默认的处理方式。如果@DataScope注解使用在形参上或者使用Service提供的方法接口,那么需要开发者手动处理,返回什么那么是开发者自定义了。所以字段你自己定,联表也没问题、反射执行什么方法、参数是什么、过程怎么样也是你自己定,灵活性很高(至少我是这么认为的,哈哈哈哈哈哈)新建DataScopeHandler@Component publicclassDataScopeHandlerimplementsDataPermissionHandler{ Map<String,ExpressStrategy>expressStrategyMap=newHashMap<>(); @PostConstruct publicvoidinit(){ expressStrategyMap.put(ExpressionEnum.EQ.toString(),newEqStrategyImpl()); expressStrategyMap.put(ExpressionEnum.NE.toString(),newNeStrategyImpl()); //....其他情况 } @Override publicExpressiongetSqlSegment(ExpressionoldWhere,StringmappedStatementId){ DataScopeAspect.DataScopeParamdataScopeParam=DataScopeAspect.getDataScopeParam(); //没有规则就不限制 if(dataScopeParam==null||dataScopeParam.getDataScopeInfo()==null||CollectionUtil.isEmpty(dataScopeParam.getDataScopeInfo().getRuleList())||SecurityUtil.isAdmin()){ returnoldWhere; } ExpressionnewWhere=null; DataScopeInfodataScopeInfo=dataScopeParam.getDataScopeInfo(); List<RuleDto>ruleList=dataScopeInfo.getRuleList(); for(RuleDtorule:ruleList){ ExpressStrategyexpressStrategy=expressStrategyMap.get(rule.getExpression()); if(expressStrategy==null) thrownewIllegalArgumentException("错误的表达式:"+rule.getExpression()); newWhere=expressStrategy.apply(rule,newWhere); } returnoldWhere==null?newWhere:newAndExpression(oldWhere,newParenthesis(newWhere)); } }使用策略模式ExpressStrategypublicinterfaceExpressStrategy{ Expressionapply(RuleDtorule,Expressionwhere); defaultObjectgetValue(RuleDtorule){ if(rule.getProvideType().equals(ProvideTypeEnum.METHOD.getCode())){ returnrule.getResult(); }elseif(rule.getProvideType().equals(ProvideTypeEnum.VALUE.getCode())){ returnrule.getValue1(); }else{ thrownewIllegalArgumentException("错误的提供类型"); } } defaultColumngetColumn(RuleDtorule){ Stringsql="".equals(rule.getTableAlias())||rule.getTableAlias()==null?rule.getColumnName():rule.getTableAlias()+"."+rule.getColumnName(); returnnewColumn(sql); } defaultbooleanisOr(StringspliceType){ if(!spliceType.equals(SpliceTypeEnum.AND.toString())&&!spliceType.equals(SpliceTypeEnum.OR.toString())){ thrownewIllegalArgumentException("错误的拼接类型:"+spliceType); } returnspliceType.equals(SpliceTypeEnum.OR.toString()); } }其中一种策略EqStrategyImpl这里只列举其中一种情况,我们处理=操作publicclassEqStrategyImplimplementsExpressStrategy{ @Override publicExpressionapply(RuleDtorule,Expressionwhere){ booleanor=isOr(rule.getSpliceType()); Columncolumn=getColumn(rule); Objectvalue=getValue(rule); StringValuevalueExpression=newStringValue((String)value); EqualsToequalsTo=newEqualsTo(column,valueExpression); if(or){ where=where==null?equalsTo:newOrExpression(where,equalsTo); }else{ where=where==null?equalsTo:newAndExpression(where,equalsTo); } returnwhere; } }注册DataScopeHandler@Configuration publicclassMyBatisPlusConfig{ @Autowired privateDataScopeHandlerdataScopeHandler; @Bean publicMybatisPlusInterceptormybatisPlusInterceptor(){ MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor(); //添加自定义的数据权限处理器 DataPermissionInterceptordataPermissionInterceptor=newDataPermissionInterceptor(); dataPermissionInterceptor.setDataPermissionHandler(dataScopeHandler); interceptor.addInnerInterceptor(dataPermissionInterceptor); //分页插件 interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.MYSQL)); returninterceptor; } }自定义注解@DataScope@Target({ElementType.METHOD,ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public@interfaceDataScope{ /** *标记这是哪个接口, */ Stringvalue(); }切面处理@Aspect @Slf4j @Component publicclassDataScopeAspect{ @Autowired privateMarkServicedataScopeService; //通过ThreadLocal记录权限相关的属性值 publicstaticThreadLocal<DataScopeParam>threadLocal=newThreadLocal<>(); publicstaticDataScopeParamgetDataScopeParam(){ returnthreadLocal.get(); } //方法切点 @Pointcut("@annotation(com.gitee.whzzone.common.annotation.DataScope)") publicvoidmethodPointCut(){ } @After("methodPointCut()") publicvoidclearThreadLocal(){ threadLocal.remove(); log.debug("threadLocal.remove()"); } @Before("methodPointCut()") publicvoiddoBefore(JoinPointpoint){ Signaturesignature=point.getSignature(); MethodSignaturemethodSignature=(MethodSignature)signature; Methodmethod=methodSignature.getMethod(); //获得注解 DataScopedataScope=method.getAnnotation(DataScope.class); try{ if(dataScope!=null&&!SecurityUtil.isAdmin()){ //拿到注解的值 StringscopeName=dataScope.value(); //根据注解的值去解析 DataScopeInfodataScopeInfo=dataScopeService.execRuleByName(scopeName); DataScopeParamdataScopeParam=newDataScopeParam(); dataScopeParam.setDataScopeInfo(dataScopeInfo); threadLocal.set(dataScopeParam); } }catch(Exceptione){ e.printStackTrace(); thrownewRuntimeException("数据权限 method 切面错误:"+e.getMessage()); } } @Data publicstaticclassDataScopeParam{ privateDataScopeInfodataScopeInfo; } }解析根据注解的值,能拿到一个mark,根据这个标记可以查询到对应的rules,则可以开始进行解析privateDataScopeInfoexecRuleHandler(List<Rule>rules){ if(CollectionUtil.isEmpty(rules)) returnnull; List<RuleDto>ruleList=newArrayList<>(); for(Rulerule:rules){ RuleDtodto=newRuleDto(); BeanUtil.copyProperties(rule,dto); if(rule.getProvideType().equals(ProvideTypeEnum.VALUE.getCode())){ ruleList.add(dto); }elseif(rule.getProvideType().equals(ProvideTypeEnum.METHOD.getCode())){ try{ Class<?>[]paramsTypes=null; Object[]argValues=null; if(StrUtil.isNotBlank(rule.getFormalParam())&&StrUtil.isNotBlank(rule.getActualParam())){ //获取形参数组 String[]formalArray=rule.getFormalParam().split(";"); //获取实参数组 String[]actualArray=rule.getActualParam().split(";"); if(formalArray.length!=actualArray.length) thrownewRuntimeException("形参数量与实参数量不符合"); //转换形参为Class数组 paramsTypes=newClass<?>[formalArray.length]; for(inti=0;i<formalArray.length;i++){ paramsTypes[i]=Class.forName(formalArray[i].trim()); } //转换实参为Object数组 argValues=newObject[actualArray.length]; for(inti=0;i<actualArray.length;i++){ argValues[i]=JSONObject.parseObject(actualArray[i],paramsTypes[i]); } } Class<?>clazz=Class.forName(rule.getClassName()); Objectresult; MethodtargetMethod=clazz.getDeclaredMethod(rule.getMethodName(),paramsTypes); if(Modifier.isStatic(targetMethod.getModifiers())){ //设置静态方法可访问 targetMethod.setAccessible(true); //执行静态方法 result=targetMethod.invoke(null,argValues); }else{ try{ //尝试从容器中获取实例 Objectinstance=context.getBean(Class.forName(rule.getClassName())); Class<?>beanClazz=instance.getClass(); MethodbeanClazzMethod=beanClazz.getDeclaredMethod(rule.getMethodName(),paramsTypes); //执行方法 result=beanClazzMethod.invoke(instance,argValues); }catch(NoSuchBeanDefinitionExceptione){ //创建类实例 Objectobj=clazz.newInstance(); //执行方法 result=targetMethod.invoke(obj,argValues); } } dto.setResult(result); ruleList.add(dto); }catch(NoSuchMethodExceptione){ thrownewRuntimeException("配置了不存在的方法"); }catch(ClassNotFoundExceptione){ thrownewRuntimeException("配置了不存在的类"); }catch(Exceptione){ e.printStackTrace(); thrownewRuntimeException("其他错误:"+e.getMessage()); } }else thrownewRuntimeException("错误的提供类型"); } DataScopeInfodataScopeInfo=newDataScopeInfo(); dataScopeInfo.setRuleList(ruleList); returndataScopeInfo; }例子1 查看订单金额大于100且小于500的订单规则配置推荐程序员摸鱼地址:https://www.yoodb.com/slack-off/home.html新增一个标记,可以理解成一个接口标识这个接口下所有的规则查看订单金额大于100且小于500的订单的需求的具体配置,这个配置的目的是通过反射执行com.gitee.whzzone.admin.business.service.impl.OrderServiceImpl这个类下的limitAmountBetween(BigDecimal, BigDecimal)的方法,也就是执行limitAmountBetween(100, 500),返回符合条件的orderIds,然后会在执行sql前去拼接select ... from order where ... and id in ({这里是返回的orderIds}),从而实现这个权限控制给角色的这个订单列表接口配置查看订单金额大于100且小于500的订单这个规则,那么这个角色只能查看范围内的订单数据了。代码controller@Api(tags="订单相关") @RestController @RequestMapping("order") publicclassOrderControllerextendsEntityController<Order,OrderService,OrderDto,OrderQuery>{ //通用的增删改查不用写,父类已实现 }servicepublicinterfaceOrderServiceextendsEntityService<Order,OrderDto,OrderQuery>{ //通用的增删改查不用写,父类已实现 /** *查询订单范围内的orderIds *@parambegin订单金额开始 *@paramend订单金额结束 *@return */ List<Long>limitAmountBetween(BigDecimalbegin,BigDecimalend); }impl@Service publicclassOrderServiceImplextendsEntityServiceImpl<OrderMapper,Order,OrderDto,OrderQuery>implementsOrderService{ @DataScope("order-list")//使用在方法上,交给AOP默认处理,标记这个方法为订单列表查询 @Override//重写父类列表查询 publicList<OrderDto>list(OrderQueryquery){ LambdaQueryWrapper<Order>queryWrapper=newLambdaQueryWrapper<>(); queryWrapper.eq(StrUtil.isNotBlank(query.getReceiverName()),Order::getReceiverName,query.getReceiverName()); queryWrapper.eq(StrUtil.isNotBlank(query.getReceiverPhone()),Order::getReceiverPhone,query.getReceiverPhone()); queryWrapper.eq(StrUtil.isNotBlank(query.getReceiverAddress()),Order::getReceiverAddress,query.getReceiverAddress()); queryWrapper.eq(query.getOrderStatus()!=null,Order::getOrderStatus,query.getOrderStatus()); returnafterQueryHandler(list(queryWrapper)); } //具体实现 @Override publicList<Long>limitAmountBetween(BigDecimalbegin,BigDecimalend){ LambdaQueryWrapper<Order>queryWrapper=newLambdaQueryWrapper<>(); queryWrapper.between(Order::getOrderAmount,begin,end); List<Order>list=list(queryWrapper); if(CollectionUtil.isEmpty(list)) returnnewArrayList<>(); returnlist.stream().map(BaseEntity::getId).collect(Collectors.toList()); } }这样就实现了查看订单金额大于100且小于500的订单的需求,其实这个需求用不着这么麻烦,被我复杂化了(演示一下),其实用例子2的方式来实现。配两条规则:分别是order_amount > 100和order_amount < 500的规则,然后选择AND连接就可以了。例子2 查看收货人地址模糊查询钦南区的订单规则配置新增一个规则,提供类型为值,单表查询可以不设置表别名,看图吧配置角色在订单列表查询接口使用的规则
-
企业开发项目SpringBoot已经是必备框架了,其中注解是开发中的小工具,用好了开发效率大大提升,当然用错了也会引入缺陷。一、Spring Web MVC与Spring Bean注解Spring Web MVC注解@RequestMapping@RequestMapping注解的主要用途是将Web请求与请求处理类中的方法进行映射。Spring MVC和Spring WebFlux都通过RquestMappingHandlerMapping和RequestMappingHndlerAdapter两个类来提供对@RequestMapping注解的支持。@RequestMapping注解对请求处理类中的请求处理方法进行标注;@RequestMapping注解拥有以下的六个配置属性:value:映射的请求URL或者其别名method:兼容HTTP的方法名params:根据HTTP参数的存在、缺省或值对请求进行过滤header:根据HTTP Header的存在、缺省或值对请求进行过滤consume:设定在HTTP请求正文中允许使用的媒体类型product:在HTTP响应体中允许使用的媒体类型提示:在使用@RequestMapping之前,请求处理类还需要使用@Controller或@RestController进行标记,公众 号Java精选,回复java面试,获取面试资料,支持在线刷题。下面是使用@RequestMapping的两个示例:@RequestMapping还可以对类进行标记,这样类中的处理方法在映射请求路径时,会自动将类上@RequestMapping设置的value拼接到方法中映射路径之前,如下:@RequestBody@RequestBody在处理请求方法的参数列表中使用,它可以将请求主体中的参数绑定到一个对象中,请求主体参数是通过HttpMessageConverter传递的,根据请求主体中的参数名与对象的属性名进行匹配并绑定值。此外,还可以通过@Valid注解对请求主体中的参数进行校验。下面是一个使用@RequestBody的示例:@GetMapping@GetMapping注解用于处理HTTP GET请求,并将请求映射到具体的处理方法中。具体来说,@GetMapping是一个组合注解,它相当于是@RequestMapping(method=RequestMethod.GET)的快捷方式。下面是@GetMapping的一个使用示例:@PostMapping@PostMapping注解用于处理HTTP POST请求,并将请求映射到具体的处理方法中。@PostMapping与@GetMapping一样,也是一个组合注解,它相当于是@RequestMapping(method=HttpMethod.POST)的快捷方式。下面是使用@PostMapping的一个示例:@PutMapping@PutMapping注解用于处理HTTP PUT请求,并将请求映射到具体的处理方法中,@PutMapping是一个组合注解,相当于是@RequestMapping(method=HttpMethod.PUT)的快捷方式。下面是使用@PutMapping的一个示例:@DeleteMapping@DeleteMapping注解用于处理HTTP DELETE请求,并将请求映射到删除方法中。@DeleteMapping是一个组合注解,它相当于是@RequestMapping(method=HttpMethod.DELETE)的快捷方式。下面是使用@DeleteMapping的一个示例:@PatchMapping@PatchMapping注解用于处理HTTP PATCH请求,并将请求映射到对应的处理方法中。@PatchMapping相当于是@RequestMapping(method=HttpMethod.PATCH)的快捷方式。下面是一个简单的示例:@ControllerAdvice@ControllerAdvice是@Component注解的一个延伸注解,Spring会自动扫描并检测被@ControllerAdvice所标注的类。@ControllerAdvice需要和@ExceptionHandler、@InitBinder以及@ModelAttribute注解搭配使用,主要是用来处理控制器所抛出的异常信息。首先,我们需要定义一个被@ControllerAdvice所标注的类,在该类中,定义一个用于处理具体异常的方法,并使用@ExceptionHandler注解进行标记。此外,在有必要的时候,可以使用@InitBinder在类中进行全局的配置,还可以使用@ModelAttribute配置与视图相关的参数。使用@ControllerAdvice注解,就可以快速的创建统一的,自定义的异常处理类。下面是一个使用@ControllerAdvice的示例代码:@ResponseBody@ResponseBody会自动将控制器中方法的返回值写入到HTTP响应中。特别的,@ResponseBody注解只能用在被@Controller注解标记的类中。如果在被@RestController标记的类中,则方法不需要使用@ResponseBody注解进行标注。@RestController相当于是@Controller和@ResponseBody的组合注解。下面是使用该注解的一个示例@ExceptionHandler@ExceptionHander注解用于标注处理特定类型异常类所抛出异常的方法。当控制器中的方法抛出异常时,Spring会自动捕获异常,并将捕获的异常信息传递给被@ExceptionHandler标注的方法。面试宝典:https://www.yoodb.com下面是使用该注解的一个示例:@ResponseStatus@ResponseStatus注解可以标注请求处理方法。使用此注解,可以指定响应所需要的HTTP STATUS。特别地,我们可以使用HttpStauts类对该注解的value属性进行赋值。下面是使用@ResponseStatus注解的一个示例:@PathVariable@PathVariable注解是将方法中的参数绑定到请求URI中的模板变量上。可以通过@RequestMapping注解来指定URI的模板变量,然后使用@PathVariable注解将方法中的参数绑定到模板变量上。特别地,@PathVariable注解允许我们使用value或name属性来给参数取一个别名。下面是使用此注解的一个示例:模板变量名需要使用{ }进行包裹,如果方法的参数名与URI模板变量名一致,则在@PathVariable中就可以省略别名的定义。下面是一个简写的示例:提示:如果参数是一个非必须的,可选的项,则可以在@PathVariable中设置require = false@RequestParam@RequestParam注解用于将方法的参数与Web请求的传递的参数进行绑定。使用@RequestParam可以轻松的访问HTTP请求参数的值。下面是使用该注解的代码示例:该注解的其他属性配置与@PathVariable的配置相同,特别的,如果传递的参数为空,还可以通过defaultValue设置一个默认值。示例代码如下:@Controller@Controller是@Component注解的一个延伸,Spring 会自动扫描并配置被该注解标注的类。此注解用于标注Spring MVC的控制器。下面是使用此注解的示例代码:@RestController@RestController是在Spring 4.0开始引入的,这是一个特定的控制器注解。此注解相当于@Controller和@ResponseBody的快捷方式。当使用此注解时,不需要再在方法上使用@ResponseBody注解。下面是使用此注解的示例代码:@ModelAttribute通过此注解,可以通过模型索引名称来访问已经存在于控制器中的model。下面是使用此注解的一个简单示例:与@PathVariable和@RequestParam注解一样,如果参数名与模型具有相同的名字,则不必指定索引名称,简写示例如下:特别地,如果使用@ModelAttribute对方法进行标注,Spring会将方法的返回值绑定到具体的Model上。示例如下:在Spring调用具体的处理方法之前,被@ModelAttribute注解标注的所有方法都将被执行。@CrossOrigin@CrossOrigin注解将为请求处理类或请求处理方法提供跨域调用支持。如果我们将此注解标注类,那么类中的所有方法都将获得支持跨域的能力。使用此注解的好处是可以微调跨域行为。使用此注解的示例如下:@InitBinder@InitBinder注解用于标注初始化WebDataBinider 的方法,该方法用于对Http请求传递的表单数据进行处理,如时间格式化、字符串处理等。下面是使用此注解的示例:二、Spring Bean注解在本小节中,主要列举与Spring Bean相关的4个注解以及它们的使用方式。@ComponentScan@ComponentScan注解用于配置Spring需要扫描的被组件注解注释的类所在的包。可以通过配置其basePackages属性或者value属性来配置需要扫描的包路径。面试宝典:https://www.yoodb.comvalue属性是basePackages的别名。此注解的用法如下:@Component@Component注解用于标注一个普通的组件类,它没有明确的业务范围,只是通知Spring被此注解的类需要被纳入到Spring Bean容器中并进行管理。此注解的使用示例如下:@Service@Service注解是@Component的一个延伸(特例),它用于标注业务逻辑类。与@Component注解一样,被此注解标注的类,会自动被Spring所管理。下面是使用@Service注解的示例:@Repository@Repository注解也是@Component注解的延伸,与@Component注解一样,被此注解标注的类会被Spring自动管理起来,@Repository注解用于标注DAO层的数据持久化类。此注解的用法如下:三、Spring Dependency Inject与Bean Scops注解Spring DI注解@DependsOn@DependsOn注解可以配置Spring IoC容器在初始化一个Bean之前,先初始化其他的Bean对象。下面是此注解使用示例代码:@Bean@Bean注解主要的作用是告知Spring,被此注解所标注的类将需要纳入到Bean管理工厂中。@Bean注解的用法很简单,在这里,着重介绍@Bean注解中initMethod和destroyMethod的用法。示例如下:Scops注解@Scope@Scope注解可以用来定义@Component标注的类的作用范围以及@Bean所标记的类的作用范围。@Scope所限定的作用范围有:singleton、prototype、request、session、globalSession或者其他的自定义范围。这里以prototype为例子进行讲解。当一个Spring Bean被声明为prototype(原型模式)时,在每次需要使用到该类的时候,Spring IoC容器都会初始化一个新的改类的实例。在定义一个Bean时,可以设置Bean的scope属性为prototype:scope=“prototype”,也可以使用@Scope注解设置,如下:@Scope(value=ConfigurableBeanFactory.SCOPE_PROPTOTYPE)下面将给出两种不同的方式来使用@Scope注解,示例代码如下:@Scope 单例模式当@Scope的作用范围设置成Singleton时,被此注解所标注的类只会被Spring IoC容器初始化一次。在默认情况下,Spring IoC容器所初始化的类实例都为singleton。同样的原理,此情形也有两种配置方式,示例代码如下:四、容器配置注解@Autowired@Autowired注解用于标记Spring将要解析和注入的依赖项。此注解可以作用在构造函数、字段和setter方法上。作用于构造函数下面是@Autowired注解标注构造函数的使用示例:作用于setter方法下面是@Autowired注解标注setter方法的示例代码:作用于字段@Autowired注解标注字段是最简单的,只需要在对应的字段上加入此注解即可,示例代码如下:@Primary当系统中需要配置多个具有相同类型的bean时,@Primary可以定义这些Bean的优先级。下面将给出一个实例代码来说明这一特性:输出结果:thisissendDingDingmethodmessage.@PostConstruct与@PreDestroy值得注意的是,这两个注解不属于Spring,它们是源于JSR-250中的两个注解,位于common-annotations.jar中。@PostConstruct注解用于标注在Bean被Spring初始化之前需要执行的方法。@PreDestroy注解用于标注Bean被销毁前需要执行的方法。下面是具体的示例代码:@Qualifier当系统中存在同一类型的多个Bean时,@Autowired在进行依赖注入的时候就不知道该选择哪一个实现类进行注入。此时,我们可以使用@Qualifier注解来微调,帮助@Autowired选择正确的依赖项。另外,推荐公众 号Java精选,回复java面试,获取资料。下面是一个关于此注解的代码示例:五、Spring Boot注解@SpringBootApplication@SpringBootApplication注解是一个快捷的配置注解,在被它标注的类中,可以定义一个或多个Bean,并自动触发自动配置Bean和自动扫描组件。此注解相当于@Configuration、@EnableAutoConfiguration和@ComponentScan的组合。在Spring Boot应用程序的主类中,就使用了此注解。示例代码如下:@SpringBootApplication publicclassApplication{ publicstaticvoidmain(String[]args){ SpringApplication.run(Application.class,args); } }@EnableAutoConfiguration@EnableAutoConfiguration注解用于通知Spring,根据当前类路径下引入的依赖包,自动配置与这些依赖包相关的配置项。@ConditionalOnClass与@ConditionalOnMissingClass这两个注解属于类条件注解,它们根据是否存在某个类作为判断依据来决定是否要执行某些配置。下面是一个简单的示例代码:@Configuration @ConditionalOnClass(DataSource.class) classMySQLAutoConfiguration{ //... }@ConditionalOnBean与@ConditionalOnMissingBean这两个注解属于对象条件注解,根据是否存在某个对象作为依据来决定是否要执行某些配置方法。示例代码如下:@Bean @ConditionalOnBean(name="dataSource") LocalContainerEntityManagerFactoryBeanentityManagerFactory(){ //... } @Bean @ConditionalOnMissingBean publicMyBeanmyBean(){ //... }@ConditionalOnProperty@ConditionalOnProperty注解会根据Spring配置文件中的配置项是否满足配置要求,从而决定是否要执行被其标注的方法。示例代码如下:@Bean @ConditionalOnProperty(name="alipay",havingValue="on") Alipayalipay(){ returnnewAlipay(); }@ConditionalOnResource此注解用于检测当某个配置文件存在使,则触发被其标注的方法,下面是使用此注解的代码示例:@ConditionalOnResource(resources="classpath:website.properties") PropertiesaddWebsiteProperties(){ //... }@ConditionalOnWebApplication与@ConditionalOnNotWebApplication这两个注解用于判断当前的应用程序是否是Web应用程序。如果当前应用是Web应用程序,则使用Spring WebApplicationContext,并定义其会话的生命周期。下面是一个简单的示例:@ConditionalOnWebApplication HealthCheckControllerhealthCheckController(){ //... }@ConditionalExpression此注解可以让我们控制更细粒度的基于表达式的配置条件限制。当表达式满足某个条件或者表达式为真的时候,将会执行被此注解标注的方法。@Bean @ConditionalException("${localstore}&&${local=='true'}") LocalFileStorestore(){ //... }@Conditional@Conditional注解可以控制更为复杂的配置条件。在Spring内置的条件控制注解不满足应用需求的时候,可以使用此注解定义自定义的控制条件,以达到自定义的要求。下面是使用该注解的简单示例:@Conditioanl(CustomConditioanl.class) CustomPropertiesaddCustomProperties(){ //... }总结本次课程总结了Spring Boot中常见的各类型注解的使用方式,让大家能够统一的对Spring Boot常用注解有一个全面的了解。
-
XXL-Job 是一个基于分布式任务调度框架的分布式任务调度平台,它提供了可视化的任务管理界面、弹性扩缩容、任务分片、失败重试等特性,适用于大规模分布式任务调度场景。 1. 引入依赖 首先,在项目的 pom.xml 文件中添加以下依赖: xml <dependency> <groupId>com.xuxueli</groupId> <artifactId>xxl-job-core</artifactId> <version>2.3.0</version> </dependency> 其中,com.xuxueli 是 XXL-Job 的开发者之一,xxl-job-core 是 XXL-Job 的核心依赖。这里我们使用的是 2.3.0 版本,你可以根据实际需要选择最新版本。 2. 配置属性 接下来,在项目的 application.properties 或 application.yml 配置文件中添加以下配置: yaml # xxl job xxl.job.admin.addresses=http://localhost:8080/xxl-job-admin xxl.job.executor.appname=my-job-app xxl.job.executor.address=http://localhost:9999/xxl-job-executor xxl.job.executor.ip= xxl.job.executor.port=9999 xxl.job.accessToken= xxl.job.logPath=/data/applogs/xxl-job/jobhandler xxl.job.logRetentionDays=30 其中,xxl.job.admin.addresses 表示 XXL-Job Admin 的地址,xxl.job.executor.address 表示 XXL-Job Executor 的地址。这里我们假设 Admin 和 Executor 都运行在本地,Executor 的端口号为 9999。 注意,xxl.job.executor.ip 需要设置为空字符串,否则 XXL-Job 会将其解析为本机 IP 地址,导致任务执行失败。 3. 编写任务处理器 然后,我们需要编写一个任务处理器来处理具体的业务逻辑。例如,我们编写一个简单的任务处理器来输出一条日志: import com.xxl.job.core.biz.model.ReturnT; import com.xxl.job.core.handler.annotation.XxlJob; import com.xxl.job.core.log.XxlJobLogger; import org.springframework.stereotype.Component; @Component public class SampleJobHandler { @XxlJob("sampleJobHandler") public ReturnT<String> sampleJobHandler(String param) throws Exception { XxlJobLogger.log("Hello, XXL-Job!"); return ReturnT.SUCCESS; } } 在上面的代码中,我们使用了 @XxlJob 注解标记了一个任务处理方法 sampleJobHandler。该方法接受一个 param 参数,并通过 XxlJobLogger.log() 方法输出日志信息。最后,使用 ReturnT.SUCCESS 表示任务执行成功。 4. 启用执行器 最后,在 Spring Boot 应用程序的启动类中添加以下代码以启用 XXL-Job 执行器: import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class Application { @Value("${xxl.job.executor.appname}") private String appName; @Value("${xxl.job.executor.address}") private String address; @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.logPath}") private String logPath; @Value("${xxl.job.logRetentionDays}") private int logRetentionDays; public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Bean public XxlJobSpringExecutor xxlJobExecutor() { XxlJobSpringExecutor executor = new XxlJobSpringExecutor(); executor.setAdminAddresses("http://localhost:8080/xxl-job-admin"); executor.setAppname(appName); executor.setAddress(address); executor.setIp(ip); executor.setPort(port); executor.setAccessToken(accessToken); executor.setLogPath(logPath); executor.setLogRetentionDays(logRetentionDays); return executor; } } 在上面的代码中,我们通过 @Value 注解和 .properties 文件中的配置属性获取 XXL-Job 执行器的相关配置信息。然后,使用 XxlJobSpringExecutor 类创建一个执行器实例,并设置相应的属性。 最后,在主类中添加 @Bean 注解,将执行器实例注入到 Spring 容器中。 5. 部署并测试 完成以上步骤后,我们可以将应用程序打成 jar 包,并在服务器上启动。然后,访问 XXL-Job Admin 的管理界面,创建一个新的任务,并指定任务处理器为 SampleJobHandler 类中的 sampleJobHandler 方法。最后,点击“启动”按钮,即可在指定时间执行该任务。
-
🥰前言 使用 Redis 进行登录适用于以下情况: 分布式系统: 当系统需要支持多个节点的分布式部署时,使用 Redis 存储登录信息能够更好地支持多节点间的共享和同步,确保用户的登录状态能够在整个系统中得到有效的传递和管理。 高并发访问: 面对大规模的并发访问,使用 Redis 可以提供更好的性能表现。Redis 是一个基于内存的高性能 Key-Value 数据库,能够更快速地读取和写入数据,因此适用于需要处理大量并发请求的场景。 灵活的数据结构需求: 如果系统需要根据业务需求选择最佳的数据结构,并且对存储和操作登录信息有更多的灵活性,那么使用 Redis 将会是一个不错的选择。Redis 支持多种数据类型的存储和操作,包括字符串、哈希表、列表、集合和有序集合等,能够满足不同的业务需求。 需要持久化支持: 如果系统需要对登录信息进行持久化存储,以防止数据丢失,Redis 的持久化功能可以很好地满足这一需求。 总的来说,使用 Redis 进行登录适用于需要支持分布式部署、面对高并发访问、有灵活的数据结构需求以及需要持久化支持的系统场景。通过合理地利用 Redis 的特性,可以更好地满足上述情况下的需求,提高系统的可扩展性、性能和稳定性。 虽然 Spring Boot 应用通常是单体应用,但是在实际运行中,我们也经常会遇到多个实例同时运行的情况,这时候就需要使用 Redis 进行分布式 Session 管理。 🛸StringRedisTemplate StringRedisTemplate是Spring Data Redis提供的一个类,它是一个具体的对象,用于操作Redis数据库中的字符串类型数据。 StringRedisTemplate封装了Redis的操作,并提供了一系列方法来对Redis中的字符串进行读取、写入和删除操作。它是RedisTemplate的一个子类,专门用于处理字符串类型的数据。 🌹使用StringRedisTemplate 首先引入依赖,引入StringRedisTemplate的依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ⭐常用的方法 StringRedisTemplate提供了多个方法来操作Redis中的字符串类型数据。下面是一些常用的方法: opsForValue().set(key, value):将一个字符串类型的值value存储到Redis中,并指定键key。 opsForValue().get(key):根据键key获取对应的字符串类型的值。 opsForValue().increment(key, delta):将键key所对应的值增加delta,delta可以为负数。 opsForValue().size(key):获取值的长度。 🛸为什么我们要使用Redis代替Session进行登录操作 集群session存在共享问题,会导致数据丢失 保存相同的数据,大家互相copy,会有内存空间的浪费 我们copy数据的时候,是需要有一定的时间的,会有延迟,如果在这个延迟之内,如果有人来访问,仍然会造成数据不一致的情况 如果我们使用Redis的话。 Redis是在tomcat外面的存储,如果任意一台tomcat都能访问到Redis,可以实现数据共享,储存在Redis里面的数据,任何tomcat都可以看到,使用就不存在数据丢失的问题 Redis读写延迟非常低,方便进行内存存储 Redis是key-value结构 🎆具体使用 ✨编写拦截器 RefreshTokenInterceptor.java 在拦截器中配置拦截操作 package com.hmdp.utils; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.StrUtil; import com.hmdp.dto.UserDTO; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Map; import java.util.concurrent.TimeUnit; import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY; import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL; public class RefreshTokenInterceptor implements HandlerInterceptor { private StringRedisTemplate stringRedisTemplate; public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.获取请求头中的token String token = request.getHeader("authorization"); if (StrUtil.isBlank(token)) { return true; } // 2.基于TOKEN获取redis中的用户 String key = LOGIN_USER_KEY + token; Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key); // 3.判断用户是否存在 if (userMap.isEmpty()) { return true; } // 5.将查询到的hash数据转为UserDTO UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); // 6.存在,保存用户信息到 ThreadLocal UserHolder.saveUser(userDTO); // 7.刷新token有效期 stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES); // 8.放行 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 移除用户 UserHolder.removeUser(); } } 刷新token的目的 用户每访问一次,这个token就会刷新一次,主要用户一直在操作,这个token就不会消失 但是如果仅仅拦截的是需要登录的路径,用户 访问 不需要登录 的路径 的时候(比如首页),这个拦截器就不生效,此时token就不会刷新,这样子,过了token的有效期后,尽管用户还在访问,用户的登录状态却消失了,这样肯定不太合理 那么我们就需要在原来的拦截器基础上再加上一个拦截器 LoginInterceptor.java 在拦截器中配置拦截操作 package com.hmdp.utils; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.判断是否需要拦截(ThreadLocal中是否有用户) if (UserHolder.getUser() == null) { // 没有,需要拦截,设置状态码 response.setStatus(401); // 拦截 return false; } // 有用户,则放行 return true; } } ✨配置拦截器 我们上面编写了拦截器,我们还需要配置拦截器,使这个拦截器生效 MvcConfig.java package com.hmdp.config; import com.hmdp.utils.LoginInterceptor; import com.hmdp.utils.RefreshTokenInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.Resource; @Configuration public class MvcConfig implements WebMvcConfigurer { @Resource private StringRedisTemplate stringRedisTemplate; @Override public void addInterceptors(InterceptorRegistry registry) { // 登录拦截器 registry.addInterceptor(new LoginInterceptor()) .excludePathPatterns( "/shop/**", "/voucher/**", "/shop-type/**", "/upload/**", "/blog/hot", "/user/code", "/user/login" ).order(1); // token刷新的拦截器 registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0); } } 🌺基于Redis实现发送手机验证码操作 🎈总体思路 🎈具体步骤 我们首先引入上面说的依赖,然后在application.yml文件(或yaml文件)中进行配置,如下 下面我们编写发送手机验证码的核心代码 @Slf4j @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result sendCode(String phone, HttpSession session) { // 1.校验手机号 if (RegexUtils.isPhoneInvalid(phone)) { // 2.如果不符合,返回错误信息 return Result.fail("手机号格式错误!"); } // 3.符合,生成验证码 String code = RandomUtil.randomNumbers(6); // 4.保存验证码到 redis stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES); // 5.发送验证码 log.debug("发送短信验证码成功,验证码:{}", code); // 返回ok return Result.ok(); } } 上面代码里面的RegexUtils.isPhoneInvalid(phone)这段代码是什么用法 stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);这段代码有什么用 这段代码的作用是将一个验证码(即code)存储到Redis中,并设置了过期时间为LOGIN_CODE_TTL分钟。以便在一定时间后自动删除该键值对。 🎍基于Redis实现短信登录并注册的操作 🎈总体思路 🎈具体步骤 我们首先引入上面说的依赖,并且在application.yml文件(或yaml文件)中进行配置(同上) 然后我们来编写核心代码 @Slf4j @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result login(LoginFormDTO loginForm, HttpSession session) { // 1.校验手机号 String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)) { // 2.如果不符合,返回错误信息 return Result.fail("手机号格式错误!"); } // 3.从redis获取验证码并校验 String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone); String code = loginForm.getCode(); if (cacheCode == null || !cacheCode.equals(code)) { // 不一致,报错 return Result.fail("验证码错误"); } // 4.一致,根据手机号查询用户 select * from tb_user where phone = ? User user = query().eq("phone", phone).one(); // 5.判断用户是否存在 if (user == null) { // 6.不存在,创建新用户并保存 user = createUserWithPhone(phone); } // 7.保存用户信息到 redis中 // 7.1.随机生成token,作为登录令牌 String token = UUID.randomUUID().toString(true); // 7.2.将User对象转为HashMap存储 UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create() .setIgnoreNullValue(true) .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); // 7.3.存储 String tokenKey = LOGIN_USER_KEY + token; stringRedisTemplate.opsForHash().putAll(tokenKey, userMap); // 7.4.设置token有效期 stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES); // 8.返回token return Result.ok(token); } private User createUserWithPhone(String phone) { // 1.创建用户 User user = new User(); user.setPhone(phone); user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)); // 2.保存用户 save(user); return user; } } User user = query().eq(“phone”, phone).one();这段代码使用了mybatisplus,相当于select * from tb_user where phone = ? 为什么要使用HashMap进行存储 在这段代码中,使用HashMap进行存储是为了将用户对象转换成键值对形式,便于统一保存到Redis中,并且可以方便地进行序列化和反序列化操作。具体来说: 便于存储和读取:将用户对象转为HashMap后,可以方便地通过stringRedisTemplate.opsForHash().putAll()方法一次性将整个用户对象存储到Redis的Hash数据结构中,而不需要对用户对象的每个字段分别进行存储。 数据结构清晰:使用HashMap可以清晰地表示用户对象的各个字段和对应的数值,便于管理和维护。 方便序列化和反序列化:HashMap作为Java中的常用数据结构,可以方便地进行序列化(将数据转换为字节序列)和反序列化(将字节序列转换为数据)操作,便于在存储到Redis或者从Redis中读取时进行数据格式的转换。 总之,使用HashMap进行存储能够简化代码逻辑,提高数据存储和读取的效率,并且方便进行数据结构的转换和管理。 Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create() .setIgnoreNullValue(true) .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); 这段代码为什么要这样写,这些参数有什么用 其中,beanToMap是一个方法,用于将Java对象(Bean)转换为Map类型的数据结构。 在这段代码中,BeanUtil.beanToMap()方法被使用,它是一个工具类方法,可以通过反射机制将Java对象的属性和对应的值转换为键值对形式,并存储到一个Map对象中。 具体来说,beanToMap方法接收三个参数: userDTO:表示要转换的源对象,即需要将其转换为Map的对象。 new HashMap<>():表示用于存储转换结果的目标HashMap对象,这里使用了一个新的空HashMap,用于接收转换后的键值对数据。 CopyOptions.create().setIgnoreNullValue(true):这是使用BeanUtil进行对象转换时的配置选项。setIgnoreNullValue(true)表示忽略源对象中值为null的属性,不将其放入目标Map中。 .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()):这个配置项表示对转换过程中的字段值进行编辑处理。在这里,它的作用是将字段值转换为字符串类型,确保最终存储到Map中的值都是字符串类型。 综合起来,这段代码的目的是将UserDTO对象转换为Map类型,同时忽略空值属性,并确保所有属性值都被转换为字符串类型。这样做的原因可能是为了在存储到Redis中时,确保数据的统一性和一致性,便于后续从Redis中读取并进行处理。 ———————————————— 版权声明:本文为CSDN博主「在下小吉.」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/m0_72853403/article/details/134946539
-
什么是配置文件 配置文件是一种文本文件,通常用于存储程序的参数和设置。它包含了程序运行所需的各种选项和配置信息,例如界面风格、服务器连接信息、默认语言等。这些信息可以通过键值对的形式表示,也可以使用特定的语法和结构描述更复杂的配置信息。 配置文件通常由程序在启动时读取和解析,以自定义程序的行为。例如,在网站开发中,网站的配置文件通常包含了数据库连接信息、安全性设置、日志记录等重要信息。在操作系统中,配置文件则可以用于指定各种系统参数,如网络连接、设备驱动程序等。 配置文件的格式和内容因程序而异,常见的配置文件格式包括INI文件、XML文件、JSON文件等。INI文件是最早的配置文件格式之一,它使用简单的键值对语法来存储配置信息。XML和JSON文件则更加灵活和可扩展,可以描述更复杂的配置信息。 除了使用传统的文本编辑器手动编写配置文件外,现在也有许多工具可以帮助用户轻松地编辑和管理配置文件。例如,一些应用程序具有自己的配置文件编辑器,用户可以通过界面来直观地编辑配置信息。另外,一些配置管理工具则可以帮助用户自动化地管理和维护配置文件,例如版本控制、备份和恢复等。 SpringBoot配置文件 SpringBoot⽀持并定义了配置⽂件的格式,也在另⼀个层⾯达到了规范其他框架集成到SpringBoot的⽬的。 很多项⽬或者框架的配置信息也放在配置⽂件中,⽐如: 项目的启动端口 数据库的连接信息(包含用户名和密码的设置) 第三⽅系统的调⽤密钥等信息 ⽤于发现和定位问题的普通⽇志和异常⽇志等 每个 Spring 项目都内置了一个 Tomcat 服务器,而 Tomcat 默认使用的端口是 8080,如果 8080 端口被占用或者多个 Spring 项目同时启动的话,就会出现某些程序无法正常启动的情况,所以这时候就可以使用 Spring 的配置文件修改启动的端口。 不仅如此,在日常生活中我们很多时候都会和数据库打交道,而每个人甚至一个人的不同机器上的数据库用户名、密码和 IP 地址都是有区别的,那么当这个项目交给别人的时候就需要在 Spring 配置文件中修改相关的配置。 不仅如此,一些公共的属性以及会根据情况而改变的属性都可以放在这个 Spring 配置文件中,通过使用 Spring 配置文件可以帮助我们解决很多的问题。 配置文件快速入手 我们先通过一个简单的例子来了解配置文件的作用。 我们创建 SpringBoot 项目的时候,会自动生成一个 application.properties 文件,这就是默认的 SpringBoot 配置文件,我们在这个配置文件中修改启动端口号看看结果。 server.port=9090 这里可以看到启动端口号修改成功了。 如果我们想要访问这个项目就不能使用 127.0.0.1:8080 了,而要使用 127.0.0.1:9090 地址了。 配置文件的格式 SpringBoot 的配置文件有三种形式: application.properties application.yml application.yaml yml 是 yaml 的缩写,两个使用方式是相同的。并且这两个配置文件还是生活中使用频率最高的。 当应⽤程序启动时,SpringBoot会⾃动从classpath路径找到并加载 application.properties 和 application.yaml 或者 application.yml ⽂件,当然也可以使用 spring.config.name 指定文件路径和名称。 那么 properties 配置文件和 yml 配置文件有什么区别呢?区别主要体现在语法和使用场景上。properties 配置格式出现的比较早,而 yml 配置格式出现的则比较晚,并且在语法上,yml 配置格式相较于 properties 配置格式来说更加的简介,并且很重要的是:properties 配置文件中如果出现中文的话出现乱码的几率非常大,而 yml 配置格式则对中文的支持度是比较高的。存在两种配置格式,可以一定程度上降低程序员的学习成本。 理论上讲 .properties 和 .yml 可以并存在于⼀个项⽬中,当 .properties 和 .yml 并存时,两个配置都会加载。如果配置⽂件内容有冲突,则以 .properties 为主,也就是 .properties 优先级更高 虽然理论上来讲 .properties 可以和 .yml 共存,但实际的业务当中,我们通常会采取⼀种统⼀的配置⽂件格式,这样可以更好的维护(降低故障率) properties 配置文件说明 properties 配置⽂件是最早期的配置⽂件格式,也是创建 SpringBoot 项⽬默认的配置⽂件。 properties 基本语法 properties 配置格式是以键值对的形式存在的,key 和 value 之间通过“=”连接。并且 key 和 value 之间可以存在多个空格。 # 配置启动端口号 server.port=9090 # 配置数据库连接信息 spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?charEncoding=utf8&userSSL=false spring.datasource.name=root spring.datasource.password=123456 配置文件中使用 # 添加注释信息。 读取配置文件信息 在项目中,如果想要主动读取配置文件中的信息的话,可以使用 @Value 注解来实现。 mykey=zhangsan package com.example.springconfiguration; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class PropertiesController { @Value("mykey") private String key; @RequestMapping("/getkey") public String getKey() { return "获取到key:" + key; } } 这是为什么呢?我们的 mykey 不是 zhangsan 吗?为什么获取到的是 mykey 呢? 这是因为 Spring 将@Value 中的 “mykey” 当作了 value 赋值给了 key,那么我们该如何才能获取到 key mykey 中的 value 值呢? 要想获取到 @Value key 中的 value 值,需要使用 ${} 来获取到这里面的 key 的 value 值。 @Value("${mykey}") properties 配置格式缺点 为什么说 properties 配置格式对中文不是很友好呢?我们来看看: mykey=张三 1 为什么就肯定说是 properties 配置文件的问题而不是我们前端接收到响应之后出现的问题呢?我们打断点看看。 这里我们读取到 mykey 的时候就是乱码,而是我的 idea 也是默认是 UTF-8 格式,是支持中文的,所以说 properties 配置文件中尽量不要出现中文。 不仅如此,通过观察 properties 配置文件中的信息,我们会发现里面的内容是比较冗余的。 以上只是 properties 的两个显著的缺点。 yml 配置文件说明 yml 是 YAML 的缩写,它的全称 Yet Another Markup Language 翻译成中⽂就是“另⼀种标记语⾔。光看 yml 的翻译就可以看出来 yml 的理想是非常远的“语言”。我们先来看看 yml 配置文件的基本语法。 yml 基本语法 yml 配置文件对于语法要求是非常严格的,他虽然也是以键值对的形式存在的,但是 key 和 value 之间必须以 冒号 + 空格 相连接,这个空格是不能省略的。 如果配置成功,那么属性就会有高亮提示。 这种灰色,就说明该属性没有配置成功,也就是语法存在问题。 空格可以有多个。 使用 yml 连接数据库 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8&userSSL=false username: root password: 123456 这种 yml 配置方式显示出了层级关系并且节省了很多代码:url、username和password属性属于datasource,而 datasource 则属于 spring。 yml 使用进阶 yml 的使用不止有这些,还有其他更厉害的用法。 yml 配置不同数据类型 前面 properties 配置格式差不多所有的配置都是字符串类型,而在 yml 配置格式中还存在布尔类型、整数类型、浮点数类型以及Null类型。 # 字符串 string.value: hello # 布尔类型 boolean.value: true boolean.value1: false # 整数类型 int.value: 10 # 浮点数 float.value: 3.14159 # Null ~ 代表null null.value: ~ 那么 yml 配置文件的属性如何读取呢?这些基本的数据类型是和 properties 配置格式是一样的,都是使用 @Value 注解来获取到指定的配置。 package com.example.springconfiguration; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class PropertiesController { @Value("${string.value}") private String stringValue; @Value("${boolean.value}") private boolean flg1; @Value("${boolean.value1}") private boolean flg2; @Value("${int.value}") private int intValue; @Value("${float.value}") private float floatValue; @Value("${null.value}") private String nullValue; @RequestMapping("/getkey") public String getKey() { return stringValue + " | " + flg1 + " | " + flg2 + " | " + intValue + " | " + floatValue + " | " + nullValue; } } 值得注意的是,在 yml 配置格式中,字符串的表现形式有三种,一种是不加引号、一种是加单引号、还有一种就是加双引号,与 properties 配置格式不同,因为 properties 配置格式都是字符串类型,所以从等号后面的第一个非空格字符开始就会被当成是字符串。 properties 配置格式 mykey1=abcde mykey2='abcde' mykey3="abcde" @RestController public class PropertiesController { @Value("${mykey1}") private String mykey1; @Value("${mykey2}") private String mykey2; @Value("${mykey3}") private String mykey3; @RequestMapping("/getkey") public String getKey() { return mykey1 + " | " + mykey2 + " | " + mykey3; } } yml 配置格式 string1: abcde string2: 'abcde' string3: "abced" @RestController public class YmlController { @Value("${string1}") private String string1; @Value("${string2}") private String string2; @Value("${string3}") private String string3; @RequestMapping("/getymlkey") public String getKey() { return string1 + " | " + string2 + " | " + string3; } } 根据输出的内容可以看到,yml 中表示字符串的方式有三种,但是这三种方式又有一些区别。 string1: abc \n de string2: 'abc \n de' string3: "abc \n ed" 1 2 3 @RestController public class YmlController { @Value("${string1}") private String string1; @Value("${string2}") private String string2; @Value("${string3}") private String string3; @RequestMapping("/getymlkey") public String getKey() { System.out.println(string1); System.out.println(string2); System.out.println(string3); return string1 + " | " + string2 + " | " + string3; } } 可以看到没有引号和单引号的用法基本上是一样的,但是双引号就会有区别,没有引号和单引号会将字符串中的一些特殊符号进行转义,使之呈现出跟字符串里面一样的内容,而双引号则不会进行转义。 配置对象 yml 中不仅能配置一些基本的数据类型,还可以配置对象。啊?我没听错吧?yml 配置格式还能配置对象?没错,你没听错,我们 yml 就是这么厉害。那么在 yml 中如何配置对象呢? student: id: 123 name: zhangsan age: 18 类似上面的配置格式就是 yml 配置对象,当然也可以使用行内写法(跟上面的写法作用是一致的)。 student1: {id: 234, name: lisi, age: 20} 1 那么如何取出配置的对象呢?这时候就不能使用 @Value 注解了,需要使用 ConfigurationProperties 注解,并且取出也需要借助类来读取。 package com.example.springconfiguration; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties(prefix = "student") public class Student { private int id; private String name; private int age; } 仅仅只是通过类来读取也是不行的,还需要保证类中的属性名称和 yml 配置文件中的对象的属性名称一致,并且类中为这些属性提供了 getter 和 setter 方法。 这是 ConfigurationProperties 注解的参数,如果不指定参数的名称的话,默认就是value/prefix。 package com.example.springconfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RequestMapping; @RestController public class StudentController { @Autowired private Student student; @RequestMapping("/readStudent") public String readStudent() { return student.toString(); } } 配置集合 yml 配置文件还可以配置集合。 dbtypes: name: - mysql - sqlserver - db2 读取 yml 配置文件中的集合同样是依赖 ConfigurationProperties 注解和类以及类中的 getter 和 setter 方法。 package com.example.springconfiguration; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.List; @Component @ConfigurationProperties(prefix = "dbtypes") @Data public class ListConfig { private List<String> name; } package com.example.springconfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ListController { @Autowired private ListConfig listConfig; @RequestMapping("/readList") public String readList() { return listConfig.toString(); } } 配置Map yml 配置格式还可以配置 Map 类型的属性。 maptypes: map: k1: China k2: America k3: Japanese 行内写法: maptypes2: {map: {k1: China, k2: America, k3: Japanese}} 1 读取 Map 类型配置也是依赖 ConfigurationProperties 注解和类以及类中的 getter 和 setter 方法。 package com.example.springconfiguration; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.Map; @Component @ConfigurationProperties(prefix = "maptypes") @Data public class MapConfig { private Map<String, String> map; } package com.example.springconfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class MapController { @Autowired private MapConfig mapConfig; @RequestMapping("/readMap") public String readMap() { return mapConfig.toString(); } } 设置不同环境的配置文件 有可能我们一个项目中针对不同的情况会有不同的配置,那么我们是否要每到一个新环境就将另一个配置文件中的内容复制到当前使用的配置文件中吗?其实没必要,一个 Spring 项目中允许存在多个配置文件,并且我们可以指定使用哪个配置文件。 但是需要注意的是,spring 中的配置文件必须以 application-xxx.yml 格式命名。比如 application-dev.yml 和 application-prod.yml,如果需要切换的话,只需要在 application.yml 配置文件中加入 spring: profiles: active: dev # application-xxx.yml 这里填 - 后面的字符 假设我们这里使用 application-dev.yml 配置文件。 YML优缺点 优点: 易读性强:YAML采用缩进和符号的方式表示层次结构,使得配置文件易于阅读和理解。 数据结构丰富:YAML支持多种数据结构类型,如标量、数组、映射等,可以方便地表示各种数据结构。 支持多语言:YAML的语法简洁,易于学习,支持多种编程语言进行解析和读取。 缺点: 错误易引入:由于YAML的缩进规则严格,如果缩进不一致或者符号使用不当,会导致解析错误。 安全性问题:由于YAML易于阅读和编写,可能存在被篡改的风险。在安全敏感的应用中,需要采取额外的安全措施。 依赖外部库:在某些编程语言中,需要依赖特定的第三方库才能解析和读取YAML文件。这可能会增加开发者的负担。 ———————————————— 版权声明:本文为CSDN博主「不能再留遗憾了」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/m0_73888323/article/details/135119136
-
前言 在现代的微服务架构中,消息队列已经成为了一个不可或缺的组件。 它能够帮助我们在不同的服务之间传递消息,并且能够确保这些消息不会丢失。 在众多的消息队列中,Kafka 是一个非常出色的选择。 它能够处理大量的实时数据,并且提供了强大的持久化能力。 在本文中,我们将会探讨如何在 SpringBoot 中整合 Kafka。 什么是Kafka? Apache Kafka 是一个开源的流处理平台,由 LinkedIn 团队开发并于 2011 年贡献给 Apache 基金会。Kafka 以其高吞吐量、可扩展性和容错性而闻名。它是一个基于发布/订阅模式的消息系统,通常用于大型实时数据流处理应用。 Kafka 的主要组件包括: Producer:负责发布消息到 Kafka 服务器。 Broker:是 Kafka 服务器实例,负责消息的存储、接收和发送。 Consumer:从 Kafka 服务器读取消息。 Topic:消息的类别或者说是消息的标签,Producer 将消息发布到特定的 Topic,Consumer 从特定的 Topic 读取消息。 Kafka 可以在分布式系统中用于构建实时流数据管道,它可以在系统或应用之间可靠地获取数据。此外,Kafka 可以和 Apache Storm、Apache Hadoop、Apache Spark 等进行集成,用于大数据处理和分析。 Kafka的应用场景? 日志收集: 一个公司可能有很多服务器,每个服务器上运行着很多服务,Kafka 可以用来实现这些服务的日志收集功能。各服务的日志分别发送到 Kafka 的不同 Topic 中。 消息系统: Kafka 能够作为一个大规模的消息处理系统,各生产者将消息发送到 Kafka,消费者从 Kafka 中读取消息进行处理。 用户活动跟踪: Kafka 也常用于用户活动跟踪和实时分析。例如,用户的点击、搜索等行为可以实时写入到 Kafka,然后进行实时或者离线分析。 在 Kafka 上可以进行实时的流处理。例如,使用 Apache Storm 集成 Kafka 来进行实时的数据处理。 指标和日志聚合: 统计数据和监控数据也是 Kafka 的一个重要应用场景。例如,通过 Kafka 可以收集各种分布式应用的数据,然后进行统一的处理和分析。 事件源: Kafka 可以作为大规模事件处理的源头,例如,用户的行为、系统的状态等都可以作为事件,通过 Kafka 进行分发处理。 示例 版本依赖 模块 版本 SpringBoot 3.1.0 JDK 17 代码 KafkaConfig @Configuration @EnableKafka public class KafkaConfig { @Bean public KafkaReceiver listener() { return new KafkaReceiver(); } } 1 2 3 4 5 6 7 8 9 10 KafkaSender @Component @Slf4j public class KafkaSender { @Resource private KafkaTemplate<String, Object> kafkaTemplate; public void send(String topic, String key, String data) { //发送消息 CompletableFuture<SendResult<String, Object>> completable = kafkaTemplate.send(topic, key, data); completable.whenCompleteAsync((result, ex) -> { if (null == ex) { log.info(topic + "生产者发送消息成功:" + result.toString()); } else { log.info(topic + "生产者发送消息失败:" + ex.getMessage()); } }); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 KafkaReceiver @Component @Slf4j public class KafkaReceiver { /** * 下面的主题是一个数组,可以同时订阅多主题,只需按数组格式即可,也就是用","隔开 */ @KafkaListener(topics = {"testTopic"}) public void receive(ConsumerRecord<?, ?> record){ log.info("消费者收到的消息key: " + record.key()); log.info("消费者收到的消息value: " + record.value().toString()); } } 1 2 3 4 5 6 7 8 9 10 11 12 KafkaController /** * kafka 测试接口 */ @RestController public class KafkaController { @Autowired private KafkaSender kafkaSender; @GetMapping("/sendMessageToKafka") public String sendMessageToKafka() { Map<String, String> messageMap = new HashMap(); messageMap.put("message", "hello world!"); ObjectMapper objectMapper = new ObjectMapper(); String data = null; try { data = objectMapper.writeValueAsString(messageMap); } catch (JsonProcessingException e) { throw new RuntimeException(e); } String key = String.valueOf(UUID.randomUUID()); //kakfa的推送消息方法有多种,可以采取带有任务key的,也可以采取不带有的(不带时默认为null) kafkaSender.send("testTopic", key, data); return "ok"; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 测试 http://127.0.0.1:8080/sendMessageToKafka 遇见问题 Error connecting to node xxxxxx:9092 (id: 0 rack: null) Error connecting to node iZbp127a9vpra4v3kmkkmzZ:9092 (id: 0 rack: null) 解决方案 修改本地物理机hosts文件。文件目录:C:\Windows\System32\drivers\etc 新增 xx.xx.xx.xx iZbp127a9vpra4v3kmkkmzZ 如果没生效,则需要重启系统 总结 通过上述的步骤,我们已经成功地在 SpringBoot 中整合了 Kafka。 这使得我们的应用程序能够在不同的服务之间传递消息,而不需要担心消息的丢失。 我们也看到,通过使用 SpringBoot,我们可以非常轻松地完成这个过程。 希望这篇文章能够帮助你在自己的项目中更好地使用 Kafka。 ———————————————— 版权声明:本文为CSDN博主「The-Venus」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/jinxinxin1314/article/details/134770887
-
🌹什么是缓存击穿 缓存击穿是指在使用缓存系统时,对一个热点数据的高并发请求导致缓存失效,多个请求同时访问数据库,造成数据库压力过大,性能下降。 具体来说,缓存击穿通常发生在以下情况下: 热点数据失效:当某个热点数据的缓存过期或被删除时,此时如果有大量的并发请求同时访问该数据,缓存系统无法命中缓存,每个请求都会直接访问数据库。 频繁更新数据:某个数据被频繁地修改,导致缓存频繁失效,而此时大量的请求同时访问该数据,造成缓存击穿。 缓存击穿会严重影响系统的性能和可用性,因为数据库无法处理如此高的并发请求,导致系统响应变慢甚至崩溃。 但是对于缓存击穿,我们有什么方法可以解决呢 🌺基于互斥锁解决问题 互斥锁(Mutex)是一种并发编程中用于保护共享资源的机制,它可以确保在同一时刻只有一个线程可以访问共享资源,从而避免多个线程同时对共享资源进行读写操作而导致的数据竞争和不确定性行为。 互斥锁的主要特点包括: 独占性:当一个线程获得了互斥锁后,其他线程就无法再获得该互斥锁,直到持有该锁的线程释放它。 阻塞和等待:如果一个线程尝试获取已被其他线程持有的互斥锁,那么它会被阻塞,直到该互斥锁被释放。 原子性:互斥锁的获取和释放操作是原子的,不会被打断。 互斥锁通常用于以下场景: 在多线程环境下保护共享资源,如共享变量、共享数据结构等,防止多个线程同时修改造成数据不一致。 控制对临界区的访问,确保同一时间只有一个线程能够执行临界区代码,以避免竞态条件(Race Condition)的发生。 🛸思路 使用互斥锁来解决缓存击穿问题的思路是通过对关键代码块进行加锁,保证在同一时间只有一个线程能够执行这段代码。这样可以有效地避免多个线程同时访问数据库,减轻数据库的压力,提高系统的性能和可用性。 在解决缓存击穿问题时,通常会使用互斥锁锁住以下几个关键步骤: 检查缓存:首先检查缓存中是否存在所需数据。 缓存失效处理:如果缓存中不存在所需数据,即缓存失效,需要进行进一步处理。 加锁:在进行缓存失效处理之前,获取互斥锁,确保只有一个线程能够执行后续的数据库查询和缓存更新操作。 数据查询和缓存更新:在成功获得互斥锁之后,执行数据库查询操作,获取所需数据,并将数据更新到缓存中。 释放锁:缓存更新完成后,释放互斥锁,允许其他等待的线程获得锁并从缓存中获取数据。 通过加锁的方式,保证了同一时间只有一个线程能够执行关键代码块,避免了缓存击穿问题。其他线程在等待期间可以从缓存中获取旧数据,而不会直接访问数据库。这样可以减少数据库的并发访问压力,提升了系统的并发能力和性能。 需要注意的是,互斥锁的使用应该谨慎,避免持有锁的时间过长,否则可能会导致其他线程的延迟和性能下降。在设计时,要权衡锁的粒度和性能需求,确保互斥锁的使用场景合理,并根据具体情况选择合适的锁机制(如读写锁、分布式锁等)进行优化。 🏳️🌈代码实现 我们看下面的例子 @Service public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService { @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result queryById(Long id) { //缓存穿透 // Shop shop=queryWithPassThrough(id); //互斥锁解决缓存击穿 Shop shop=queryWithMutex(id); if(shop==null){ return Result.fail("店铺不存在"); } //返回 return Result.ok(shop); } public Shop queryWithMutex(Long id){ String key=CACHE_SHOP_KEY+":"+id; //从redis中查询缓存 String shopJson=stringRedisTemplate.opsForValue().get(key); //判断是否存在 if(StrUtil.isNotBlank(shopJson)){ //存在,直接返回 return JSONUtil.toBean(shopJson, Shop.class); } //判断命中的是否是空值 if(shopJson!=null){ //返回一个错误信息 return null; } //实现缓存重建 //获取互斥锁 String lockKey="lock:shop"+id; Shop shop=null; try { boolean isLock=tryLock(lockKey); //判断是否获取成功 if (!isLock){ //失败,那么休眠并且重试 Thread.sleep(100); return queryWithMutex(id); } //成功,则根据id查询数据库 shop=getById(id); //不存在,返回错误 if(shop==null){ //将空值写入到redis stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES); return null; } //存在,写入到redis里面 stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES); }catch (Exception e){ throw new RuntimeException(e); }finally { //释放互斥锁 unlock(lockKey); } //返回 return shop; } //存在,写入到redis里面 stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES); //返回 return shop; } //获取锁 private boolean tryLock(String key){ Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } //释放锁 private void unlock(String key){ stringRedisTemplate.delete(key); } } ———————————————— 版权声明:本文为CSDN博主「在下小吉.」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/m0_72853403/article/details/135145363
-
引言 在项目开发过程中,随着项目不断扩大,也就是业务的不断增多,我们将采用集群,微服务的形式去降低配置,集群是指将一个项目部署到多个服务器当中,而微服务是指将业务拆分为一个一个服务去进行,那么我们想要去实现一个业务的时候,我们就需要调用多个服务,那么每个服务的调用,我们都需要知道它的URL,若是有成千上万的URL该如何维护呢? 这时就需要注册中心功能 其实我们可以把注册中心类比与通讯录 设计思路 程序是数据结构和算法的组合,那么我们若是想要设计一个注册中心,我们就要从两个方面去考虑 存储结构 提供的操作 存储结构 我们类比一下通讯录,通讯录的每一个内容都包括姓名和电话,那么注册中心每一个保存的是服务名和服务信息,这个结构我们不难想象到map结构 则注册表的存储结构如下: 两层map结构: 在微服务系统中,为了避免单点故障,通常使用的是服务集群,一个服务名会对应多个服务实例 注册中心的操作 接收服务 接收服务心跳 接收服务下线 服务剔除 查询注册表中服务信息 注册中心集群中的信息同步 注册中心的使用 注册中心的使用主要分为两步 搭建注册中心服务端 启动业务服务,让服务和注册中心连通 本文的核心内容就是具体的注册中心配置使用 Dubbo和Zookeeper的使用 Zookeeper zookeeper的安装 安装地址 将conf目录下的zoo_sample.cfg文件,复制一份,重命名为zoo.cfg 在步骤2的基础上,修改zoo.cfg配置文件中zookeeper的数据文件存储位置及添加zookeeper日志文件位置(方法查错),(数据日志配置需另加),具体步骤如下 在zookeeper目录下新建tmp文件夹,在分别tmp文件夹下面新建data文件夹与log文件夹,用于存放data(数据存储)和log(数据日志) 修改conf下zoo.cfg配置中数据存储和数据日志路径 dataDir=../tmp/data dataLogDir=../tmp/log # 添加此配置项 admin.serverPort=8888 zoo.cfg配置文件参数说明 # tickTime 客户端与服务器或者服务器与服务器之间维持心跳,也就是每个tickTime时间就会发送一次心跳。通过心跳不仅能够用来监听机器的工作状态,还可以通过心跳来控制Flower跟Leader的通信时间 # initLimit 集群中的follower服务器(F)与leader服务器(L)之间初始连接时能容忍的最多心跳数 #syncLimit 集群中flower服务器(F)跟leader(L)服务器之间的请求和答应最多能容忍的心跳数 # dataDir 用来存放myid信息跟一些版本,日志,跟服务器唯一的ID信息等。 # dataLogDir 存放事务日志的文件夹 # clientPort 客户端连接的接口,客户端连接zookeeper服务器的端口,zookeeper会监听这个端口,接收客户端的请求访问!这个端口默认是2181。 # maxClientCnxns 允许连接的客户端数目,0为不限制,通过IP来区分不同的客户端 判断zookeeper安装十分成功 bin目录 zkCli.cmd 验证是否安装成功 zkServer.cmd 启动zookeeper服务 可以通过连接工具判断是否连接成功 下载地址——prettyzoo dubbo-admin dubbo-admin下载 安装地址 后端启动打包 踩坑经过:一定要确定运行环境是jdk1.8 成功启动并打包 前端启动打包 首先找到前端文件夹ui 执行命令 npm install 1 打包即可,注意要检查是否完成node.js的安装 Eureka的使用 搭建eureka-server步骤 1.引入SpringCloud为eureka提供的starter依赖: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> 2. 编写一个application.yml文件,内容如下: server: port: 10086 spring: application: name: eureka-server eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka 3. @EnableEurekaServer注解 @SpringBootApplication @EnableEurekaServer public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); } } 4.启动微服务,然后在浏览器访问:http://127.0.0.1:10086 Nacos的使用 1. 下载安装 下载地址——Nacos下载 Nacos安装: 解压安装包 打开配置文件(nacos安装目录 -> conf -> application.properties) spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC db.user=root db.password=123456 修改端口号 \### Default web context path: server.servlet.contextPath=/nacos \### Default web server port: server.port=8848 执行sql文件(nacos安装目录 -> conf ->nacos-mysql.sql) 通过修改nacos.core.auth.enabled为true,就可以开启nacos认证。开启后客户端必须配置username、password。 单机模式启动 在nacs安装目录下的bin目录中运行命令,默认是集群模式启动,单机模式启动需要指定模式。 Windows命令:startup.cmd -m standalone Linux命令:sh startup.sh -m standalone 命令运行成功后直接访问:http://localhost:8848/nacos 默认账号密码都是:nacos Consul的使用 下载与安装 下载地址——Consul 1)下载 wget https://releases.hashicorp.com/consul/1.3.0/consul_1.3.0_linux_amd64.zip 1 2)解压 unzip consul_1.3.0_linux_amd64.zip 1 3)拷贝到usr目录下 mv consul /usr 1 4)查看是否安装成功 ./consul ———————————————— 版权声明:本文为CSDN博主「雪碧有白泡泡」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/Why_does_it_work/article/details/134777376
-
什么是配置文件 配置文件是一种文本文件,通常用于存储程序的参数和设置。它包含了程序运行所需的各种选项和配置信息,例如界面风格、服务器连接信息、默认语言等。这些信息可以通过键值对的形式表示,也可以使用特定的语法和结构描述更复杂的配置信息。 配置文件通常由程序在启动时读取和解析,以自定义程序的行为。例如,在网站开发中,网站的配置文件通常包含了数据库连接信息、安全性设置、日志记录等重要信息。在操作系统中,配置文件则可以用于指定各种系统参数,如网络连接、设备驱动程序等。 配置文件的格式和内容因程序而异,常见的配置文件格式包括INI文件、XML文件、JSON文件等。INI文件是最早的配置文件格式之一,它使用简单的键值对语法来存储配置信息。XML和JSON文件则更加灵活和可扩展,可以描述更复杂的配置信息。 除了使用传统的文本编辑器手动编写配置文件外,现在也有许多工具可以帮助用户轻松地编辑和管理配置文件。例如,一些应用程序具有自己的配置文件编辑器,用户可以通过界面来直观地编辑配置信息。另外,一些配置管理工具则可以帮助用户自动化地管理和维护配置文件,例如版本控制、备份和恢复等。 SpringBoot配置文件 SpringBoot⽀持并定义了配置⽂件的格式,也在另⼀个层⾯达到了规范其他框架集成到SpringBoot的⽬的。 很多项⽬或者框架的配置信息也放在配置⽂件中,⽐如: 项目的启动端口 数据库的连接信息(包含用户名和密码的设置) 第三⽅系统的调⽤密钥等信息 ⽤于发现和定位问题的普通⽇志和异常⽇志等 每个 Spring 项目都内置了一个 Tomcat 服务器,而 Tomcat 默认使用的端口是 8080,如果 8080 端口被占用或者多个 Spring 项目同时启动的话,就会出现某些程序无法正常启动的情况,所以这时候就可以使用 Spring 的配置文件修改启动的端口。 不仅如此,在日常生活中我们很多时候都会和数据库打交道,而每个人甚至一个人的不同机器上的数据库用户名、密码和 IP 地址都是有区别的,那么当这个项目交给别人的时候就需要在 Spring 配置文件中修改相关的配置。 不仅如此,一些公共的属性以及会根据情况而改变的属性都可以放在这个 Spring 配置文件中,通过使用 Spring 配置文件可以帮助我们解决很多的问题。 配置文件快速入手 我们先通过一个简单的例子来了解配置文件的作用。 我们创建 SpringBoot 项目的时候,会自动生成一个 application.properties 文件,这就是默认的 SpringBoot 配置文件,我们在这个配置文件中修改启动端口号看看结果。 server.port=9090 1 这里可以看到启动端口号修改成功了。 如果我们想要访问这个项目就不能使用 127.0.0.1:8080 了,而要使用 127.0.0.1:9090 地址了。 配置文件的格式 SpringBoot 的配置文件有三种形式: application.properties application.yml application.yaml yml 是 yaml 的缩写,两个使用方式是相同的。并且这两个配置文件还是生活中使用频率最高的。 当应⽤程序启动时,SpringBoot会⾃动从classpath路径找到并加载 application.properties 和 application.yaml 或者 application.yml ⽂件,当然也可以使用 spring.config.name 指定文件路径和名称。 那么 properties 配置文件和 yml 配置文件有什么区别呢?区别主要体现在语法和使用场景上。properties 配置格式出现的比较早,而 yml 配置格式出现的则比较晚,并且在语法上,yml 配置格式相较于 properties 配置格式来说更加的简介,并且很重要的是:properties 配置文件中如果出现中文的话出现乱码的几率非常大,而 yml 配置格式则对中文的支持度是比较高的。存在两种配置格式,可以一定程度上降低程序员的学习成本。 理论上讲 .properties 和 .yml 可以并存在于⼀个项⽬中,当 .properties 和 .yml 并存时,两个配置都会加载。如果配置⽂件内容有冲突,则以 .properties 为主,也就是 .properties 优先级更高 虽然理论上来讲 .properties 可以和 .yml 共存,但实际的业务当中,我们通常会采取⼀种统⼀的配置⽂件格式,这样可以更好的维护(降低故障率) properties 配置文件说明 properties 配置⽂件是最早期的配置⽂件格式,也是创建 SpringBoot 项⽬默认的配置⽂件。 properties 基本语法 properties 配置格式是以键值对的形式存在的,key 和 value 之间通过“=”连接。并且 key 和 value 之间可以存在多个空格。 # 配置启动端口号 server.port=9090 # 配置数据库连接信息 spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?charEncoding=utf8&userSSL=false spring.datasource.name=root spring.datasource.password=123456 配置文件中使用 # 添加注释信息。 读取配置文件信息 在项目中,如果想要主动读取配置文件中的信息的话,可以使用 @Value 注解来实现。 mykey=zhangsan 1 package com.example.springconfiguration; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class PropertiesController { @Value("mykey") private String key; @RequestMapping("/getkey") public String getKey() { return "获取到key:" + key; } } 这是为什么呢?我们的 mykey 不是 zhangsan 吗?为什么获取到的是 mykey 呢? 这是因为 Spring 将@Value 中的 “mykey” 当作了 value 赋值给了 key,那么我们该如何才能获取到 key mykey 中的 value 值呢? 要想获取到 @Value key 中的 value 值,需要使用 ${} 来获取到这里面的 key 的 value 值。 @Value("${mykey}") properties 配置格式缺点 为什么说 properties 配置格式对中文不是很友好呢?我们来看看: mykey=张三 1 为什么就肯定说是 properties 配置文件的问题而不是我们前端接收到响应之后出现的问题呢?我们打断点看看。 这里我们读取到 mykey 的时候就是乱码,而是我的 idea 也是默认是 UTF-8 格式,是支持中文的,所以说 properties 配置文件中尽量不要出现中文。 不仅如此,通过观察 properties 配置文件中的信息,我们会发现里面的内容是比较冗余的。 以上只是 properties 的两个显著的缺点。 yml 配置文件说明 yml 是 YAML 的缩写,它的全称 Yet Another Markup Language 翻译成中⽂就是“另⼀种标记语⾔。光看 yml 的翻译就可以看出来 yml 的理想是非常远的“语言”。我们先来看看 yml 配置文件的基本语法。 yml 基本语法 yml 配置文件对于语法要求是非常严格的,他虽然也是以键值对的形式存在的,但是 key 和 value 之间必须以 冒号 + 空格 相连接,这个空格是不能省略的。 如果配置成功,那么属性就会有高亮提示。 这种灰色,就说明该属性没有配置成功,也就是语法存在问题。 空格可以有多个。 使用 yml 连接数据库 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8&userSSL=false username: root password: 123456 这种 yml 配置方式显示出了层级关系并且节省了很多代码:url、username和password属性属于datasource,而 datasource 则属于 spring。 yml 使用进阶 yml 的使用不止有这些,还有其他更厉害的用法。 yml 配置不同数据类型 前面 properties 配置格式差不多所有的配置都是字符串类型,而在 yml 配置格式中还存在布尔类型、整数类型、浮点数类型以及Null类型。 # 字符串 string.value: hello # 布尔类型 boolean.value: true boolean.value1: false # 整数类型 int.value: 10 # 浮点数 float.value: 3.14159 # Null ~ 代表null null.value: ~ 那么 yml 配置文件的属性如何读取呢?这些基本的数据类型是和 properties 配置格式是一样的,都是使用 @Value 注解来获取到指定的配置。 package com.example.springconfiguration; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class PropertiesController { @Value("${string.value}") private String stringValue; @Value("${boolean.value}") private boolean flg1; @Value("${boolean.value1}") private boolean flg2; @Value("${int.value}") private int intValue; @Value("${float.value}") private float floatValue; @Value("${null.value}") private String nullValue; @RequestMapping("/getkey") public String getKey() { return stringValue + " | " + flg1 + " | " + flg2 + " | " + intValue + " | " + floatValue + " | " + nullValue; } } 值得注意的是,在 yml 配置格式中,字符串的表现形式有三种,一种是不加引号、一种是加单引号、还有一种就是加双引号,与 properties 配置格式不同,因为 properties 配置格式都是字符串类型,所以从等号后面的第一个非空格字符开始就会被当成是字符串。 properties 配置格式 mykey1=abcde mykey2='abcde' mykey3="abcde" @RestController public class PropertiesController { @Value("${mykey1}") private String mykey1; @Value("${mykey2}") private String mykey2; @Value("${mykey3}") private String mykey3; @RequestMapping("/getkey") public String getKey() { return mykey1 + " | " + mykey2 + " | " + mykey3; } } yml 配置格式 string1: abcde string2: 'abcde' string3: "abced" @RestController public class YmlController { @Value("${string1}") private String string1; @Value("${string2}") private String string2; @Value("${string3}") private String string3; @RequestMapping("/getymlkey") public String getKey() { return string1 + " | " + string2 + " | " + string3; } } 根据输出的内容可以看到,yml 中表示字符串的方式有三种,但是这三种方式又有一些区别。 string1: abc \n de string2: 'abc \n de' string3: "abc \n ed" @RestController public class YmlController { @Value("${string1}") private String string1; @Value("${string2}") private String string2; @Value("${string3}") private String string3; @RequestMapping("/getymlkey") public String getKey() { System.out.println(string1); System.out.println(string2); System.out.println(string3); return string1 + " | " + string2 + " | " + string3; } } 可以看到没有引号和单引号的用法基本上是一样的,但是双引号就会有区别,没有引号和单引号会将字符串中的一些特殊符号进行转义,使之呈现出跟字符串里面一样的内容,而双引号则不会进行转义。 配置对象 yml 中不仅能配置一些基本的数据类型,还可以配置对象。啊?我没听错吧?yml 配置格式还能配置对象?没错,你没听错,我们 yml 就是这么厉害。那么在 yml 中如何配置对象呢? student: id: 123 name: zhangsan age: 18 类似上面的配置格式就是 yml 配置对象,当然也可以使用行内写法(跟上面的写法作用是一致的)。 student1: {id: 234, name: lisi, age: 20} 1 那么如何取出配置的对象呢?这时候就不能使用 @Value 注解了,需要使用 ConfigurationProperties 注解,并且取出也需要借助类来读取。 package com.example.springconfiguration; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties(prefix = "student") public class Student { private int id; private String name; private int age; } 仅仅只是通过类来读取也是不行的,还需要保证类中的属性名称和 yml 配置文件中的对象的属性名称一致,并且类中为这些属性提供了 getter 和 setter 方法。 这是 ConfigurationProperties 注解的参数,如果不指定参数的名称的话,默认就是value/prefix。 package com.example.springconfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RequestMapping; @RestController public class StudentController { @Autowired private Student student; @RequestMapping("/readStudent") public String readStudent() { return student.toString(); } } 配置集合 yml 配置文件还可以配置集合。 dbtypes: name: - mysql - sqlserver - db2 读取 yml 配置文件中的集合同样是依赖 ConfigurationProperties 注解和类以及类中的 getter 和 setter 方法。 package com.example.springconfiguration; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.List; @Component @ConfigurationProperties(prefix = "dbtypes") @Data public class ListConfig { private List<String> name; } package com.example.springconfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ListController { @Autowired private ListConfig listConfig; @RequestMapping("/readList") public String readList() { return listConfig.toString(); } } 配置Map yml 配置格式还可以配置 Map 类型的属性。 maptypes: map: k1: China k2: America k3: Japanese 行内写法: maptypes2: {map: {k1: China, k2: America, k3: Japanese}} 1 读取 Map 类型配置也是依赖 ConfigurationProperties 注解和类以及类中的 getter 和 setter 方法。 package com.example.springconfiguration; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.Map; @Component @ConfigurationProperties(prefix = "maptypes") @Data public class MapConfig { private Map<String, String> map; } package com.example.springconfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class MapController { @Autowired private MapConfig mapConfig; @RequestMapping("/readMap") public String readMap() { return mapConfig.toString(); } } 设置不同环境的配置文件 有可能我们一个项目中针对不同的情况会有不同的配置,那么我们是否要每到一个新环境就将另一个配置文件中的内容复制到当前使用的配置文件中吗?其实没必要,一个 Spring 项目中允许存在多个配置文件,并且我们可以指定使用哪个配置文件。 但是需要注意的是,spring 中的配置文件必须以 application-xxx.yml 格式命名。比如 application-dev.yml 和 application-prod.yml,如果需要切换的话,只需要在 application.yml 配置文件中加入 spring: profiles: active: dev # application-xxx.yml 这里填 - 后面的字符 1 2 3 假设我们这里使用 application-dev.yml 配置文件。 YML优缺点 优点: 易读性强:YAML采用缩进和符号的方式表示层次结构,使得配置文件易于阅读和理解。 数据结构丰富:YAML支持多种数据结构类型,如标量、数组、映射等,可以方便地表示各种数据结构。 支持多语言:YAML的语法简洁,易于学习,支持多种编程语言进行解析和读取。 缺点: 错误易引入:由于YAML的缩进规则严格,如果缩进不一致或者符号使用不当,会导致解析错误。 安全性问题:由于YAML易于阅读和编写,可能存在被篡改的风险。在安全敏感的应用中,需要采取额外的安全措施。 依赖外部库:在某些编程语言中,需要依赖特定的第三方库才能解析和读取YAML文件。这可能会增加开发者的负担。 ———————————————— 版权声明:本文为CSDN博主「不能再留遗憾了」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/m0_73888323/article/details/135119136
推荐直播
-
华为云IoT开源专家实践分享:开源让物联网平台更开放、易用
2024/05/14 周二 16:30-18:00
张俭 华为云IoT DTSE技术布道师
作为开发者的你是否也想加入开源社区?本期物联网平台资深“程序猿”,开源专家张俭,为你揭秘华为云IoT如何借助开源构建可靠、开放、易用的物联网平台,并手把手教你玩转开源社区!
去报名 -
华为云开发者日·广州站
2024/05/15 周三 14:30-17:30
华为云专家团
华为云开发者日HDC.Cloud Day是面向全球开发者的旗舰活动,汇聚来自千行百业、高校及科研院所的开发人员。致力于打造开发者专属的技术盛宴,全方位服务与赋能开发者围绕华为云生态“知、学、用、创、商”的成长路径。通过前沿的技术分享、场景化的动手体验、优秀的应用创新推介,为开发者提供沉浸式学习与交流平台。开放创新,与开发者共创、共享、共赢未来。
去报名 -
企业级数仓迁移工具助您轻松上云
2024/05/21 周二 16:30-18:00
Nick 华为云数仓GaussDB(DWS)研发专家
随着云时代的到来,传统数仓已无法满足企业的需求,越来越多的企业选择从传统数仓迁移到云数据仓库,云数仓为企业提供了更低的成本、更灵活极致的体验。本期直播将为您带来企业级数仓搬迁的解决方案,带您一览华为云数仓GaussDB(DWS)提供了哪些迁移方案助力用户实现数据迁移,如何保障迁移后数据的一致性。
去报名
热门标签