- ControllerAdvice 注解的三种使用场景
- 1. 引言
- 2. Spring Boot 的特点
- 3. Spring Boot 的约定
- 4. Spring Boot 环境搭建
- 5. Spring Boot 初始工程
- 6. Spring Boot 自定义 banner
- 7. Spring Boot 全局延迟初始化(懒加载)
- 8. Spring Boot 配置文件拆分
- 9. Spring Boot 注入语法
- 10. Spring Boot 集成 页面模板
- 11. Thymelefa 常见语法介绍
- 12. MyBatis 与 Spring Boot 整合
- 13. MyBatis-plus 与 Spring Boot整合
- 14. Spring Boot 热部署
- 15. logback 日志的集成
- 16. Spring Boot 拦截器
- 17. Spring Boot 过滤器
- 17. Spring Boot 对表单数据校验
- 18. Spring Boot 异常处理方式
- 19. Spring Boot 整合 junit 单元测试
- 20. Spring Boot 异步处理
- 21. Spring Boot 邮件发送
- 22. Spring Boot 下定时任务
- 23. Redis 与 Spring Boot 整合
- 19. Reids 分布式 Session 管理
- 19. kapthca 三方验证码与Spring Boot 整合
- 20. Spring Boot 将本地磁盘作为静态资源访问
- 21. Spring Boot 拦截器 注入对象失败解决方法
- 22. Nginx 反向代理配置
- 23. Nginx 负载均衡
- 24. HttpServletRequest 获取用户信息
- 25. 获取用户IP地址
- 26. 获取用户浏览器信息和操作系统信息
- 27. shedlock 解决分布式定时任务重复执行问题
- 28. Redis解决分布式定时任务重复执行问题
- Spring Boot 部署方式
- Spring Boot 常用注解介绍
- yml / properties 配置文件介绍
- swagger2 添加head头部信息
- 2. Spring Boot 的特点
优点
- 快速构建一个独立的Spring应用程序
- 嵌入的Tomcat, Jetty 或者 Undertow , 无需部署WAR文件
- 提供Starter Poms 来简化Maven配置和减少版本冲突所带来的的问题
- 对Spring和第三方库提供默认配置, 也可以修改默认值, 简化框架配置
- 提供生成就绪功能, 如: 指示, 健康检查和外部配置
- 无需配置XML, 无代码生成, 开箱即用
Why Spring Boot
ControllerAdvice 注解的三种使用场景
@ControllerAdvice ,很多初学者可能都没有听说过这个注解,实际上,这是一个非常有用的注解,顾名思义,这是一个增强的 Controller。使用这个 Controller ,可以实现三个方面的功能:
- 全局异常处理
- 全局数据绑定
- 全局数据预处理
灵活使用这三个功能,可以帮助我们简化很多工作,需要注意的是,这是 SpringMVC 提供的功能,在 Spring Boot 中可以直接使用,下面分别来看。
1. 全局异常处理
使用 @ControllerAdvice 实现全局异常处理,只需要定义类,添加该注解即可定义方式如下:
@ControllerAdvicepublic class MyGlobalExceptionHandler {@ExceptionHandler(Exception.class)public ModelAndView customException(Exception e) {ModelAndView mv = new ModelAndView();mv.addObject("message", e.getMessage());mv.setViewName("myerror");return mv;}}
在该类中,可以定义多个方法,不同的方法处理不同的异常,例如专门处理空指针的方法、专门处理数组越界的方法…,也可以直接向上面代码一样,在一个方法中处理所有的异常信息。
@ExceptionHandler 注解用来指明异常的处理类型,即如果这里指定为 NullpointerException,则数组越界异常就不会进到这个方法中来。
2. 全局数据绑定
全局数据绑定功能可以用来做一些初始化的数据操作,我们可以将一些公共的数据定义在添加了 @ControllerAdvice 注解的类中,这样,在每一个 Controller 的接口中,就都能够访问导致这些数据。
使用步骤,首先定义全局数据,如下:
@ControllerAdvicepublic class MyGlobalExceptionHandler {@ModelAttribute(name = "md")public Map<String,Object> mydata() {HashMap<String, Object> map = new HashMap<>();map.put("age", 99);map.put("gender", "男");return map;}}
使用 @ModelAttribute 注解标记该方法的返回数据是一个全局数据,默认情况下,这个全局数据的 key 就是返回的变量名,value 就是方法返回值,当然开发者可以通过 @ModelAttribute 注解的 name 属性去重新指定 key。
定义完成后,在任何一个Controller 的接口中,都可以获取到这里定义的数据:
@RestControllerpublic class HelloController {@GetMapping("/hello")public String hello(Model model) {Map<String, Object> map = model.asMap();System.out.println(map);int i = 1 / 0;return "hello controller advice";}}
3. 全局数据预处理
考虑我有两个实体类,Book 和 Author,分别定义如下:
public class Book {private String name;private Long price;//getter/setter}public class Author {private String name;private Integer age;//getter/setter}
此时,如果我定义一个数据添加接口,如下:
@PostMapping("/book")public void addBook(Book book, Author author) {System.out.println(book);System.out.println(author);}
这个时候,添加操作就会有问题,因为两个实体类都有一个 name 属性,从前端传递时 ,无法区分。此时,通过 @ControllerAdvice 的全局数据预处理可以解决这个问题
解决步骤如下:
给接口中的变量取别名
@PostMapping("/book")public void addBook(@ModelAttribute("b") Book book, @ModelAttribute("a") Author author) {System.out.println(book);System.out.println(author);}
进行请求数据预处理
在 @ControllerAdvice 标记的类中添加如下代码:
@InitBinder("b")public void b(WebDataBinder binder) {binder.setFieldDefaultPrefix("b.");}@InitBinder("a")public void a(WebDataBinder binder) {binder.setFieldDefaultPrefix("a.");}
@InitBinder(“b”) 注解表示该方法用来处理和Book和相关的参数,在方法中,给参数添加一个 b 前缀,即请求参数要有b前缀.
- 发送请求
请求发送时,通过给不同对象的参数添加不同的前缀,可以实现参数的区分
本教程是基于 Spring Boot 2.2.5 版本的学习
1. 引言
Spring Boot 是由Pivotal团队提供的全新框架,其设计的目的是用来简化 Spring 应用,初始搭建以及开发工程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这中方式,Spring Boot 致力于在蓬勃发展的快速应用开发领域成为领导者。
2. Spring Boot 的特点
- 创建独立的Spring应用程序
- 嵌入Tomcat,无需部署WAR文件
- 简化Maven配置
- 自动配置Spring,没有XML配置
3. Spring Boot 的约定
在 Spring Boot 中约定大于配置
- Spring Boot 项目必须在src/main/resources 中放入application.yml(properties) 核心配置,名字必须是‘’application‘
- Spring Boot 项目必须在src/main/java 中所有子包之外构建全局入口类型,xxxApplication.入口类一个Spring Boot 项目只能够有一个。
4. Spring Boot 环境搭建
5. Spring Boot 初始工程
- Spring Boot 项目必须继承 SpringBoot 父工程
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.5.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent>
- 引入 Spring Boot 工程所需要的JAR
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
Spring Boot 想已 jar 运行需要引入对应的插件
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin>
6. Spring Boot 自定义 banner
- 定制Banner
- 在resources 下创建banner.txt 文档
- 在文档中编写自己喜欢的内容即可
- 关闭Banner
SpringApplicationBuilder builder = new SpringApplicationBuilder(App.class);SpringApplication build = builder.build();build.setBannerMode(Banner.Mode.OFF);build.run(args);
7. Spring Boot 全局延迟初始化(懒加载)
Spring Boot 默认懒加载是关闭的,如果需要开启懒加载,只需要在配置文件中指定即可
spring:main:lazy-initialization: true
8. Spring Boot 配置文件拆分
Spring Boot 默认启动加载的配置文件是(application.yml/properties),当配置文件才分后我们可以在主配置文件中指定使用那个配置文件有效
spring:profiles:active: dev
9. Spring Boot 注入语法
@Value(“${keyName}”) 取普通数据
@Value(“#{${maps}}”) 取 Map 集合数据
@ConfigurationProperties(“keyName”) 将定义的数据注入到一个对象中
10. Spring Boot 集成 页面模板
- 集成 JSP
- thymeleaf
- freemarker
- Spring Boot 整合 JSP
引入 JSP 相关依赖
<!-- JSP 标准标签库 --><dependency><groupId>javax.servlet</groupId><artifactId>jstl</artifactId><version>1.2</version></dependency><!-- 引入解析 jsp 页面的依赖 --><dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-jasper</artifactId></dependency><build><plugins><!-- 引入 JSP 运行插件 --><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
配置 JSP 试图解析器
spring.mvc.view.prefix=/WEB-INF/jsp/spring.mvc.view.suffix=.jspserver.servlet.jsp.init-parameters.development = true # 开启 JSP 页面热部署,即修改了 JSP 页面内容就发布。
- Spring Boot 整合 Thymeleaf
Thymeleaf是一款用于渲染XML/XHTML/HTML5内容的模板引擎。类似JSP,FreeMaker等, 它也可以轻易的与 Web 框架进行集成作
为 Web 应用的模板引擎。与其它模板引擎相比, Thymeleaf 最大的特点是能够直接在浏览器中打开并正确显示模板页面,而不需要
启动整个Web应用。
thymeLeaf支持Spring Expression Language语言作为方言,也就是SpEL,SpEL是可以用于Spring中的一种EL表达式。它与我们使用
过的JSP不同,thymeleaf是使用html的标签来完成逻辑和数据的传入进行渲染。
可以说用 thymeleaf 完全替代 jsp 是可行的。
引入Thymeleaf 依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>
配置视图解析器
# Thymeleaf 下面四个配置默认格式可以不用配置。# 前缀,模板目录spring.thymeleaf.prefix=classpath:/templates/# 后缀spring.thymeleaf.suffix=.html# 编码格式spring.thymeleaf.charset=UTF-8# 浏览器响应类型spring.thymeleaf.servlet.content-type=text/html# 设置静态目录spring.resources.static-locations=classpath:/templates/,classpath:/static/
Thymeleaf 默认是无法访问,必须通过控制器去访问
Thymeleaf 页面想使用Thymeleaf 提供的标签语法,必须引入对应的命名空间
<html lang="en" xmlns:th="http://www.thymele
- Spring Boot 整合 Freemarker
配置试图解析器
#freemarkerspring.freemarker.template-loader-path=classpath:/templates/# 后缀spring.freemarker.suffix=.htmlspring.freemarker.charset=UTF-8spring.freemarker.content-type=text/html
11. Thymelefa 常见语法介绍
Thymeleaf 页面想使用Thymeleaf 提供的标签语法,必须引入对应的命名空间
<html lang="en" xmlns:th="http://www.thymele
- 获取单个数据
<span th:text="${keyName}">th:text 标签会将HTML标签转为文本直接显示</span><span th:html="${keyName}">th:html 标签会将HTML解析出来展示</span><input type="text" th:value="${keyName}" /> th:value 将数据赋值给标签的value属性。
- 获取对象数据
<span th:text="${object.name}">获取对象或者集合数据时直接对象/集合.属性</span><span th:text="${#dates.format(object.name,'yyyy-MM-dd')}"></span>
- 条件判断
<span th:if="${user.age} le 20" th:text="${user.name}"></span>
gt >lt <ge >=le <=eq ==ne !=
- 循环展示多个数据
<ul th:each="user:${users}"><li th:text="${user.id}"></li></ul>
th:each 如何获取遍历状态
<ul th:each="user,userStat:${users}">userStat , 表示Thymelefa 遍历的状态<span th:text="${userStat.size}">遍历次数</span><span th:text="${userStat.index}">遍历的索引,下标,0开始</span><span th:text="${userStat.count}">当前遍历的次数</span><span th:text="${userStat.even}">是否时偶数</span><span th:text="${userStat.odd}">是否是奇数</span><span th:text="${userStat.current}">是否时当前遍历</span><span th:text="${userStat.first}">是否是第一个</span><span th:text="${userStat.last}">是否是最后一个</span></ul>
- thymelefa 如何获取静态资源
th:href="@{/css/main.css}"th:src="@{/js/inc.js}"
- thymelefa 请求传递参数
<a th:href="@{/user/delete(id=21,name='小张')}">删除会员</a>
12. MyBatis 与 Spring Boot 整合
13. MyBatis-plus 与 Spring Boot整合
14. Spring Boot 热部署
引入jar
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional></dependency><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><fork>true</fork></configuration></plugin>
IDEA 设置自动编译
# 1. 开启自动编译Preferences | build, Excution, Deployment | Compiler -> 勾选上 Build project automatically 这个选项# 2. 开启允许运行过程中修改文件ctrl + alt + shift + / ---> 选择 Registry ---> 勾选 compiler.automarke.allow.when.app.running 这个选项。
15. logback 日志的集成
- logback 简介
Logback 是由log4j 创始人设计的又一个开源日志组件。目前,logback 分为三个模块,logback-core , logback-classic , logback-access。 是对log4j日志展示进一步改良。
- 日志级别
> DEBUG < INFO < WARN < ERROR日志级别由低到高,越高级别输出的日志信息越少
- 项目中日志分类
# 项目中日志级别分两类: rootLogger , 用来监听项目中所有的运行日志,包括引入依赖jar中的日志。logger , 用来监听项目中指定的日志信息
- Java 项目中如何使用
logback 配置文件
注意 : logback 的配置文件必须放在项目根目录中,且名字必须为logback.xml
<?xml version="1.0" encoding="UTF-8" ?><configuration><!--控制台输出 --><appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender"><!-- 定义项目中日志输出格式 --><layout class="ch.qos.logback.classic.PatternLayout"><pattern>[%p] %d{yyyy-MM-dd HH:mm:ss} [%L] [%c] %m %n</pattern></layout></appender><!-- 定义项目中根日志控制 --><root level="info"><appender-ref ref="consoleLog" /></root><!-- 项目中日志包日志控制,一般用来输出MyBatis SQL 语句--><logger name="com.example.payment.core.dao" level="DEBUG"/></configuration>
日志信息代表的含义
-X号: X信息输出时左对齐;%p: 输出日志信息优先级,即DEBUG,INFO,WARN,ERROR,FATAL,%d: 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921%r: 输出自应用启动到输出该log信息耗费的毫秒数%c: 输出日志信息所属的类目,通常就是所在类的全名%t: 输出产生该日志事件的线程名%l: 输出日志事件的发生位置,相当于%C.%M(%F:%L)的组合,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main (TestLog4.java:10)%x: 输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像Java servlets这样的多客户多线程的应用中。%%: 输出一个”%”字符%F: 输出日志消息产生时所在的文件名称%L: 输出代码中的行号%m: 输出代码中指定的消息,产生的日志具体信息%n: 输出一个回车换行符,Windows平台为”\r\n”,Unix平台为”\n”输出日志信息换行
16. Spring Boot 拦截器
作用 通过拦截器中执行通用代码逻辑,以减少控制器中冗余代码
特点 1. 拦截器只能够拦截控制器相关请求,不能够了拦截静态资源和页面的相关请求,css img
2. 请求发送经过拦截器,响应回来同样经过拦截器3. 拦截器中断用户请求4. 拦截器可以针对性拦截某些控制器请求
拦截器开发步骤
- 编写类实现 HandlerInterceptor 接口
- 重写方法
- preHandle 预处理(true,放行,false 中断)
- postHandle 请求过程中执行(controller 执行后的操作)
- afterCompletion 最终处理
- 编写拦截器配置类
编写拦截器
public class LoginHandlerInterceptor implements HandlerInterceptor {// 预处理(true,放行,false 中断)@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession();Member member = (Member) session.getAttribute(GlobalConst.MEMBER_INFO);if (StringUtils.isEmpty(member)) {request.setAttribute("msg", "没有权限请先登录");request.getRequestDispatcher("/page/login").forward(request, response);return false;}return true;}// 请求过程中执行(controller 执行后的操作)@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);}// 最终处理@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {HandlerInterceptor.super.afterCompletion(request, response, handler, ex);}}
拦截器配置类
@Configurationpublic class LoginConfiguration implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {LoginHandlerInterceptor loginHandlerInterceptor = new LoginHandlerInterceptor();// 注册拦截器InterceptorRegistration loginRegistry = registry.addInterceptor(loginHandlerInterceptor);// 添加拦截的路径loginRegistry.addPathPatterns("/**");// 排除路径loginRegistry.excludePathPatterns("/");loginRegistry.excludePathPatterns("/login");loginRegistry.excludePathPatterns("/kaptcha");loginRegistry.excludePathPatterns("/myKaptcha");loginRegistry.excludePathPatterns("/pay/**");loginRegistry.excludePathPatterns("/file/**");loginRegistry.excludePathPatterns("/page/**");loginRegistry.excludePathPatterns("/sysUser/login");loginRegistry.excludePathPatterns("/sysUser/register");loginRegistry.excludePathPatterns("/api/**");// 排除资源请求loginRegistry.excludePathPatterns("/static/**");loginRegistry.excludePathPatterns("/css/**");loginRegistry.excludePathPatterns("/images/**");loginRegistry.excludePathPatterns("/js/**");loginRegistry.excludePathPatterns("/lib/**");}}
17. Spring Boot 过滤器
它是基于Servlet 技术实现的, 简单的来说,过滤器就是起到过滤的作用,在web项目开发中帮我们过滤一些指定的 url做一些特殊的
处理。 过滤器主要做什么?
- 过滤掉一些不需要的东西,例如一些错误的请求。
- 也可以修改请求和相应的内容。
- 也可以拿来过滤未登录用户
过滤器的代码实现
过滤器(filter)有三个方法,其中初始化(init)和摧毁(destroy)方法一般不会用到,主要用到的是doFilter这个方法。
springBoot中如何使用过滤器
自定义Filter有两种实现方式,第一种是使用@WebFilter,第二种是使用 FilterRegistrationBean,下面我们分别来实现
- @WebFilter 实现
@WebFilter 用于将一个类声明为过滤器,该注解将会在部署时被容器处理,容器将根据具体的属性配置将相应的类部署为过滤器。
| 属性名 | 类型 | 描述 |
|---|---|---|
| filterName | String | 指定该Filter的名称 |
| urlPatterns | String | 指定该Filter所拦截的URL。 |
| value | String | 与 urlPatterns 一致 |
- 创建类,实现 Filter
import org.springframework.stereotype.Component;import javax.servlet.*;import javax.servlet.annotation.WebFilter;import javax.servlet.http.HttpServletRequest;import java.io.IOException;@WebFilter(urlPatterns = "/api/*",filterName = "myFilter")@Order(1)//指定过滤器的执行顺序,值越大越靠后执行public class MyFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {System.out.println("初始化过滤器");}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request= (HttpServletRequest) servletRequest;String uri=request.getRequestURI();String method=request.getMethod();System.out.println(uri+" "+method+"哈哈我进入了 MyFilter 过滤器了");filterChain.doFilter(servletRequest,servletResponse);}}
- 启动类加上 @ServletComponentScan 注解
- 创建一个 FilterController 接口
import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/api")public class HelloController {@GetMapping("/user/filter")public String hello(){return "哈哈我通过了过滤器";}}
17. Spring Boot 对表单数据校验
Spring Boot 中默认采用Hibernate validata 对数据进行校验
- 在被校验的实体类中添加校验规则
- 在需要校验的Controller 方法添加 @Valid 注解
@NotBlank(message = "账号不能够为空 !")private String account;@NotBlank(message = "密码不能够为空 !")@Size(min = 8, max = 20, message = "密码长度必须是8-20之间")private String password;
/*** @Valid 开启对user的数据校验* BindingResult 封装了校验结果*/@RequestMapper("/save")public R saveOrUpdage(@Valid User user,BindingResult result){if(result.hasErrors()){// 返回 true 表示有校验失败的错误信息return R.error(1,"数据校验失败 !");}return R.success("");}
thymeleaf 试图下如何获取校验错误信息
<font color="red" th:errors="${user.name}"></font>
常见注解介绍
| 注解 | 注解说明 | ||
|---|---|---|---|
| @Null | 被注释的元素必须为 null | @NotNull | 被注释的元素必须不为 null |
| @AssertTrue | 被注释的元素必须为 true | @AssertFalse | 被注释的元素必须为 false |
| @Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 | @Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
| @DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 | @DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
| @Size(max=, min=) | 被注释的元素的大小必须在指定的范围内 | @Digits (integer, fraction) |
被注释的元素必须是一个数字,其值必须在可接受的范围内 |
| @Past | 被注释的元素必须是一个过去的日期 | @Future | 被注释的元素必须是一个将来的日期 |
| @Pattern(regex=,flag=) | 被注释的元素必须符合指定的正则表达式 | @Range(min=,max=,message=) | 被注释的元素必须在合适的范围内 |
| @NotBlank(message =“”) |
验证字符串非null,且长度必须大于0 | 被注释的元素必须是电子邮箱地址 | |
| @Length(min=,max=) | 被注释的字符串的大小必须在指定的范围内 | @NotEmpty | 被注释的字符串的必须非空 |
18. Spring Boot 异常处理方式
- 自定义错误页面
- @ExceptionHandler注解处理异常
- @ControllerAdvice + @ExceptionHandler注解处理异常
- 配置 SimpleMappingExceptionResolver 处理异常
- 自定义 HandlerExceptionResolver 类处理异常
自定义错误页面
Spring Boot 默认的处理异常机制:Spring Boot 默认已经提供了一套处理异常机制,程序中出现了异常,Spring Boot 会像 /error 的URL 发送请求。在Spring Boot 中提供了一个叫BassicExceptionController 来处理 /error 请求。然后跳转到默认显示页面来展示异常信息
Whitelabel Error PageThis application has no explicit maping for /erro,so you are seeing this as a fallack.Wed mar 28 11:18:37 GMT + 08:00 2020There was an unexpected error (type=Internal Server Error,status=500).NO message available
- 在 templates 新建错误页面(error.html)页面名称必须叫error
- 在页面中编写自己的错误代码
出错了,请联系管理员。<span th:text="${exception}"></span>
@ExceptionHandler 注解处理异常
- 在需要处理异常的 Controller 中编写处理方法
@ExceptionHandler(UnknownAccountException.class)public R unknownAccountException(Exception e) {// 异常处理方式return R.error(1, "账号密码错误!");}
此中处理方式只能够处理对应 Controller 中异常,其他 Controller 中异常无法处理,且需要编写大量的方法和页面处理。[不推荐]
@ControllerAdvice + @ExceptionHandler注解处理异常 【推荐方式】
- 编写异常处理类,并且继承 RuntimeException
- 在类中编写需要拦截处理的异常
@RestControllerAdvicepublic class GlobalExceptionHandler extends RuntimeException {@Autowiredprivate ExceptionLogService loginService;@ExceptionHandler(value = Exception.class)public R exceptionHandler(HttpServletRequest request, Exception e) {log.error("最大异常错误(Exception):{}", e);this.saveExport(request, e); // 将错误信息保存在数据库中。return R.error(50001, e.getMessage());}@ExceptionHandler(UnknownAccountException.class)public R unknownAccountException(HttpServletRequest request, UnknownAccountException e) {log.error("账号错误!,exception:{}", e);this.saveExport(request, e); // 将错误信息保存在数据库中。return R.error(1, "账号密码错误!");}@ExceptionHandler(NullPointerException.class)public R NullPointerException(HttpServletRequest request, NullPointerException e) {log.error("空指针异常!,exception:{}", e);this.saveExport(request, e); // 将错误信息保存在数据库中。return R.error(1, "必传递参数为空!");}public void saveExport(HttpServletRequest request, Exception e) {ExceptionLog log = new ExceptionLog();log.setCreateTime(new Date());log.setExceptionMsg(this.getStackTrace(e));log.setMessage(e.getMessage());log.setLogType(1);log.setUrl(request.getRequestURI());log.setType(request.getMethod());log.setParam(request.getQueryString());log.setIp(StringUtil.getIPAddress(request));loginService.save(log);}public String getStackTrace(Exception e) {StringBuffer message = new StringBuffer();StackTraceElement[] exceptionStack = e.getStackTrace();message.append(e.toString());for (StackTraceElement ste : exceptionStack) {message.append("\n\tat " + ste);}return message.toString();}}
- @ExceptionHandler 表示拦截异常
- @ControllerAdvice 是 controller 的一个辅助类,最常用的就是作为全局异常处理的切面类
- @ControllerAdvice 可以指定扫描范围
- @ControllerAdvice 约定了几种可行的返回值,如果是直接返回 model 类的话,需要使用 @ResponseBody 进行 json 转换
返回 String,表示跳到某个 view。返回 modelAndView。返回 model + @ResponseBod
配置 SimpleMappingExceptionResolver 处理异常
自定义 HandlerExceptionResolver 类处理异常
19. Spring Boot 整合 junit 单元测试
- 引入相关 jar
<!-- Spring Boot Test --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency><!-- junit Test --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><scope>test</scope></dependency>
- 编写测试类
@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest(classes = PaymentApp.class)public class SpringbootAsyncApplicationTests {@Autowiredprivate RedisTemplate redisTemplate;@Testpublic void contextLoad() {// RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();// connection.flushDb(); // 刷新数据// connection.flushAll(); // 刷新所有数据库redisTemplate.opsForValue().set("name", "张三");String name = (String) redisTemplate.opsForValue().get("name");System.out.println(name);}}
SpringBootTest 中 class 指定 spring boot 启动类。
20. Spring Boot 异步处理
Spring Boot 中异步处理方法不能够和业务方法在同一个类,否则异步方法无效
- 编写异步处理方法的类
@Servicepublic class AsynService{@Asyncpublic void asynEmail(){// 异步处理逻辑代码}}
- Spring Boot 启动类开启异步处理
@EnableAsync
- 在需要用到异步处理的逻辑处调用对应方法即可
21. Spring Boot 邮件发送
- 引入对应jar
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency>
- 配置邮件相关内容(properties/yml)
spring.mail.username = 2324234234@qq.comspring.mail.password = 12adfrq230rawspring.mail.host = smtp.qq.comspring.mail.properties.mail.smtp.ssl.enable=true # 开启加密验证
- 编写邮件发送代码
@Autowiredprivate JavaMailSenderImpl mailSender;public void senderMail(){SimpleMailMessage mailMessage = new SimpleMailMessage();mailMessage.setSubject("邮件标题");mailMessage.setText("邮件内容");mailMessage.setTo("接收邮件地址");mailMessage.setFrom("发送邮件的地址");mailSender.send(mailMessage);}
22. Spring Boot 下定时任务
- 开启定时任务注解(Spring Boot 启动类中)
@EnableScheduling
- 编写定时任务
@Componentpublic class ScheduService{// 秒,分,时,日,月,周,年@Scheduled(cron = "0 2 0 * * ?") // 方法具体执行时间;cron 在线表达式生成器(http://cron.qqe2.com)public void executeTask() {// 编写定时任务逻辑代码}}
23. Redis 与 Spring Boot 整合
- 引入相关依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
- 编写 redis 链接配置信息
spring:redis:host: 127.0.0.1port: 6379database: 2password: asvdsafaew
- 编写代码
19. Reids 分布式 Session 管理
分布式部署服务器,session在各自服务器下保存不便宜管理,且有问题,这个时候就可以将所有的服务器下的session统一交给Redis服务管理
- 添加依赖
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency>
- 编写配置类(RedisSessionManagerApplication)
@Configuration@EnableRedisHttpSession // 将整个应用中使用的session的数据全部交给redis存储public class RedisSessionManagerApplication {// 完成以上两步后session就统一移交给redis管理,但是注意,每次修改数据后都要重新保存(session.setAttribute()),否则redis中保存的数据将不会同步}
19. kapthca 三方验证码与Spring Boot 整合
- 添加依赖
<dependency><groupId>com.github.penggle</groupId><artifactId>kaptcha</artifactId><version>2.3.2</version></dependency>
- 在 resources 下创建 kaptcha 配置信息
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!-- 生成kaptcha的bean --><bean id="captchaProducer" class="com.google.code.kaptcha.impl.DefaultKaptcha"><property name="config"><bean class="com.google.code.kaptcha.util.Config"><constructor-arg type="java.util.Properties"><!--设置kaptcha属性 --><props><!-- 图片边框 --><prop key="kaptcha.border ">no</prop><!-- 边框颜色 --><prop key="kaptcha.border.color">white</prop><!-- 验证码颜色 --><prop key="kaptcha.textproducer.font.color">blue</prop><!-- 宽度 --><prop key="kaptcha.image.width">100</prop><!-- 高度 --><prop key="kaptcha.image.height">50</prop><!-- 字体大小 --><prop key="kaptcha.textproducer.font.size">21</prop><!-- 边框厚度(合法值 ,大于0 的整数) --><prop key="kaptcha.border.thickness">1</prop><!-- 验证码码长度 --><prop key="kaptcha.textproducer.char.length">4</prop><!-- 验证码内容 --><prop key="kaptcha.textproducer.char.string">23456789ABCDEFGHJKLMNPQRSTUVWXYZ</prop><!-- 图片实现类 --><prop key="kaptcha.obscurificator.impl">com.google.code.kaptcha.impl.WaterRipple</prop><!-- 干扰线颜色 --><prop key="kaptcha.noise.color">white</prop><!-- 干扰线类型 com.google.code.kaptcha.impl.DefaultNoise --><prop key="kaptcha.noise.impl">com.google.code.kaptcha.impl.NoNoise</prop><!-- 背景颜色渐变,开始颜色 --><prop key="kaptcha.background.clear.from">185,56,213</prop><!-- 背景颜色渐变,结束颜色 --><prop key="kaptcha.background.clear.to">white</prop><!-- 文字间隔 --><prop key="kaptcha.textproducer.char.space">5</prop><!-- session key --><!-- <prop key="kaptcha.session.key">code</prop> --></props></constructor-arg></bean></property></bean></beans>
- 在Spring Boot 启动类添加如下注解
@ImportResource(locations = { "classpath:kaptcha.xml" })
- 编写Controller
@RequestMapping("/kaptcha")public void getKaptchaImage(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpSession session = request.getSession();response.setDateHeader("Expires", 0);response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");response.addHeader("Cache-Control", "post-check=0, pre-check=0");response.setHeader("Pragma", "no-cache");response.setContentType("image/jpeg");// 生成验证码String capText = captchaProducer.createText();session.setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);// 向客户端写出BufferedImage bi = captchaProducer.createImage(capText);ServletOutputStream out = response.getOutputStream();ImageIO.write(bi, "jpg", out);try {out.flush();} finally {out.close();}}
- 验证用户验证码是否正确
// 获取生成的验证码String verifyCodeExpected = (String) request.getSession().getAttribute(com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY);
20. Spring Boot 将本地磁盘作为静态资源访问
- 直接将本地路径作为静态资源加载
spring.resources.static-locations = classpath:/templates/,classpath:/static/,file:${upload.dir}upload.dir = D:/home/h-payment
spring.resources.static-locations 中添加 file:D:/home/h-payment 即可
<img alt="" src="icon/2020-10-28-13-17-12--V8MLlpB57HsdseEd.jpg">
html 页面中直接src 拼接 【file】后面的目录和资源文件即可
- 实现 WebMvcConfigurer 接口的方式
@Configurationpublic class UploadFilePathConfig implements WebMvcConfigurer {@Autowiredprivate SysFileSettingMapper fileSettingMapper;@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {try {// addResourceHandler 静态资源对外暴露的访问路径// addResourceLocations 文件上传实际保存的目录,这里需要注意文件夹后面必须得带上斜杠,否则会出现404的问题registry.addResourceHandler("/file/**").addResourceLocations("file:/" + this.getFileParentPath() + File.separator);} catch (FileNotFoundException e) {e.printStackTrace();}}public String getFileParentPath() throws FileNotFoundException {SysFileSetting fileSetting = fileSettingMapper.selectOne(new QueryWrapper<SysFileSetting>().like("keyword", "系统文件保存路径"));String osName = System.getProperty("os.name").toLowerCase();// 系统属性,如:Windows,Linuxif (Pattern.matches("linux.*", osName)) {if (null == fileSetting) {return "/home/fileUpload";}if (StringUtils.isEmpty(fileSetting.getLinux())) {return "/home/fileUpload";}return fileSetting.getLinux();} else if (Pattern.matches("windows.*", osName)) {if (null == fileSetting) {return "C:/home/fileUpload";}if (StringUtils.isEmpty(fileSetting.getWindows())) {return "C:/home/fileUpload";}return fileSetting.getWindows();}return ResourceUtils.getURL("classpath:").getPath() + "/static/upload";}}
此时文件的访问就是 file/文件本地后于路径
21. Spring Boot 拦截器 注入对象失败解决方法
问题场景
1、拦截器需要实现HandlerInterceptor接口;
2、使用WebMvcConfigurer接口用来注册拦截器;
3、拦截器中无法自动注入spring bean,使用@Autowired注解或者是构造器注入的对象是null。
解决方案
Demo demo = org.springframework.web.context.support.WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext()).getBean(Demo.class);
22. Nginx 反向代理配置
server{listen 80; # 监听的端口autoindex on;server_name www.xwzap.com; # 监听端口下的域名access_log /home/logs/www.xwzap.com_ngnix.log combined;index index.html index htm index.jsp index.php;#error_page 404 /404.html;if ( $query_string ~* ".*[\;'\<\>].*" ){return 404;}location /{proxy_pass http://www.xwzap.com:8081; # 反向代理到哪里去add_header Access-Control-Allow-Origin *;}}
表示监听www.xwzap.com域名下的80端口,反向代理到www.xwzap.com:8081端口
23. Nginx 负载均衡
upstream xwzap{server 172.30.151.19:8081;server 172.30.151.19:8082;}server{listen 80;autoindex on;server_name www.xwzap.com;access_log /home/logs/www.xwzap.com_ngnix.log combined;index index.html index htm index.jsp index.php;#error_page 404 /404.html;if ( $query_string ~* ".*[\;'\<\>].*" ){return 404;}location /{proxy_pass http://xwzap; # proxy_pass 表示转发到端口add_header Access-Control-Allow-Origin *;}}
proxy_pass 后面的转发端口必须是 upstream 取的名称上面配置表示:监听80端口下的 www.xwzap.com 请求,将起反向代理到xwzap(proxy_pass)
24. HttpServletRequest 获取用户信息
request.getRequestURI(); // 请求URLrequest.getMethod(); // 请求方式request.getQueryString(); // 请求参数
25. 获取用户IP地址
public static String getIPAddress(HttpServletRequest request) {String ip = null;// X-Forwarded-For:Squid 服务代理String ipAddresses = request.getHeader("X-Forwarded-For");if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {// Proxy-Client-IP:apache 服务代理ipAddresses = request.getHeader("Proxy-Client-IP");}if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {// WL-Proxy-Client-IP:weblogic 服务代理ipAddresses = request.getHeader("WL-Proxy-Client-IP");}if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {// HTTP_CLIENT_IP:有些代理服务器ipAddresses = request.getHeader("HTTP_CLIENT_IP");}if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {// X-Real-IP:nginx服务代理ipAddresses = request.getHeader("X-Real-IP");}// 有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IPif (ipAddresses != null && ipAddresses.length() != 0) {ip = ipAddresses.split(",")[0];}// 还是不能获取到,最后再通过request.getRemoteAddr();获取if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {ip = request.getRemoteAddr();}return ip;}
26. 获取用户浏览器信息和操作系统信息
- 添加依赖
<dependency><groupId>eu.bitwalker</groupId><artifactId>UserAgentUtils</artifactId><version>1.21</version></dependency>
- 编写代码
UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));Browser browser = userAgent.getBrowser();OperatingSystem operatingSystem = userAgent.getOperatingSystem();Version browserVersion = userAgent.getBrowserVersion();String browser = browser.getName(); //浏览器信息String browserVersion = browserVersion.getVersion(); // 浏览器版本String operatingSystem = operatingSystem.getName();//操作系统信息
27. shedlock 解决分布式定时任务重复执行问题
在集群模式部署服务端时,会出现所有的定时任务在各自的节点处均会执行一遍,这显然不符合实际的开发场景,针对这种问题,本文给出一种springboot集成shedlock的解决方案
- 引入相关包
<!-- 负载均衡定时任务执行一次 --><dependency><groupId>net.javacrumbs.shedlock</groupId><artifactId>shedlock-spring</artifactId><version>2.2.1</version></dependency><!-- 持久化采用的mysql数据库,所以引入的是JDBC数据库进行协调 --><dependency><groupId>net.javacrumbs.shedlock</groupId><artifactId>shedlock-provider-jdbc-template</artifactId><version>2.2.1</version></dependency>
ShedLock还可以使用Mongo,Redis,Hazelcast,ZooKeeper等外部存储进行协调,例如使用redis则引入下面的包(只尝试过jdbc方式):
<dependency><groupId>net.javacrumbs.shedlock</groupId><artifactId>shedlock-provider-redis-spring</artifactId><version>2.5.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
- 创建表结构
CREATE TABLE shedlock (NAME VARCHAR (64) PRIMARY KE,lock_until TIMESTAMP (3) NULL,locked_at TIMESTAMP (3) NULL,locked_by VARCHAR (255))
- 在application.properties中添加数据库配置信息
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=truespring.datasource.username=root1spring.datasource.password=root1
- 添加配置类
import net.javacrumbs.shedlock.core.LockProvider;import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;import net.javacrumbs.shedlock.spring.ScheduledLockConfiguration;import net.javacrumbs.shedlock.spring.ScheduledLockConfigurationBuilder;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.TaskScheduler;import org.springframework.scheduling.annotation.EnableScheduling;import javax.sql.DataSource;import java.time.Duration;@Configuration@EnableSchedulingpublic class ShedlockConfig {@Beanpublic LockProvider lockProvider(DataSource dataSource) {return new JdbcTemplateLockProvider(dataSource);}// @Bean// public TaskScheduler taskScheduler(){// return new MySpecialTaskScheduler();// }@Beanpublic ScheduledLockConfiguration scheduledLockConfiguration(LockProvider lockProvider) {return ScheduledLockConfigurationBuilder.withLockProvider(lockProvider).withPoolSize(10).withDefaultLockAtMostFor(Duration.ofMinutes(10)).build();}}
- 在启动类上添加启动注解,否则SchedulerLock不会生效
@EnableSchedulerLock(defaultLockAtMostFor = "PT50S")
- 添加@SchedulerLock到定时器业务方法入口
private static final int TWENTY_NINE_MIN = 29 * 60 * 1000;@Scheduled(cron = "0 */30 * * * ?")@SchedulerLock(name = "scheduledTask", lockAtMostFor = TWENTY_NINE_MIN, lockAtLeastFor = TWENTY_NINE_MIN)public void scheduledTask() {System.out.println(new Date() + "scheduledTask执行1次");// 业务方法}
参数解释
- name属性:锁名称,必须指定,每次只能执行一个具有相同名字的任务,锁名称应该是全局唯一的;
- lockAtMostFor属性:设置锁的最大持有时间,为了解决如果持有锁的节点挂了,无法释放锁,其他节点无法进行下一次任务;
- lockAtMostForString属性:成功执行任务的节点所能拥有的独占锁的最长时间的字符串表达,例如“PT14M”表示为14分钟
- lockAtLeastFor属性:指定保留锁的最短时间。主要目的是在任务非常短的且节点之间存在时钟差异的情况下防止多个节点执行。这个属性是锁的持有时间。设置了多少就一定会持有多长时间,再此期间,下一次任务执行时,其他节点包括它本身是不会执行任务的
- lockAtLeastForString属性:成功执行任务的节点所能拥有的独占锁的最短时间的字符串表达,例如“PT14M”表示为14分钟
28. Redis解决分布式定时任务重复执行问题
问题描述: 有一个定时任务是每周一给客户发送邮件的功能, 后台部署了2台服务器,所以客户 收到了2封重复邮件。
解决思路
分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。
这里使用一台Redis服务器来解决上面的问题。代码部分比较简单:
加锁 :主要是给多个定时任务给redis加锁(key),如果存在key,则加锁失败,如果不存在,则尝试去加锁,返回加锁结果。
解锁: 设置一下过期时间为20秒(可根据任务执行长短调整),过期后自动释放掉。这里就不去代码里面释放锁了。
Spring Boot 部署方式
- war 包部署
- pom.xml 指定部署方式
<packaging>war</packaging>
- 指定入口类
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><fork>true</fork><!-- 增加 jvm 参数 --><jvmArguments>-Dfile.encodeing=UTF-8</jvmArguments><!-- 指定入口类路径 --><mainClass>com.xxx.vp.HPlayer</mainClass></configuration></plugin>
如何是 JSP 页面需要排除内嵌Tomcat
<!-- 排除内嵌 Tomcat --><dependency><groupId>org.springframwork.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></dependency><!-- 排除 Spring Boot 内嵌 Tomcat 解析 JSP --><dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-jasper</artifactId><scope>provided</scope></dependency>
修改入口类,继承 SpringBootServletInitializer 重写 configure 方法
public class App extends SpringBootServletInitializer {public static void main(String[] args) {SpringApplication.run(App.class, args);log.info("App 启动成功!");}@Overrideprotected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {return builder.sources(App.class)}}
- jar 包部署
Spring Boot 常用注解介绍
- @Bean — 用来将该方法返回值交给SpringBoot管理,在工厂中默认标识:类名首字母小写。@Bean修饰的对象默认是单例。如果想改为非单例,可以通过注解(@Scope(“prototype”))修饰即可。
@Configuration — 用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。
注意@Configuration 不可以是final类型;@Configuration 不可以是匿名类;嵌套的configuration必须是静态类。
@Autowired —表示被修饰的类需要注入对象,spring会扫描所有被@Autowired标注的类,然后根据 类型在ioc容器中找到匹配的类注入。
yml / properties 配置文件介绍
- yml 中定义一个变量
age = 18 # 定义一个变量name = 张三,lishi,王五 # 定义一个数组maps = {'aa':'zhangsan','bb':'lishi','cc':'wangwu'} # 定义一个Map<String,String> 集合user.id = 12 # 将数据注入到对象中user.age = 18user.name = 李四
Map 集合取值是不一样,采用@Value(“#{${maps}}”)
swagger2 添加head头部信息
public Docket createRestApi() {// =====添加head参数start============================/*** 这是为了我们在用 swagger 测试接口的时候添加头部信息*/List<Parameter> pars = new ArrayList<Parameter>();ParameterBuilder tokenPar = new ParameterBuilder();tokenPar.name("sessionId"); // 头部 head 在后台获取的 KEYtokenPar.description("swagger测试用(模拟sessionId传入)非必填 header"); // 描述tokenPar.modelRef(new ModelRef("string"));tokenPar.parameterType("header");tokenPar.required(false);/*** 多个的时候 就直接添加到 pars 就可以了*/pars.add(tokenPar.build());// =========添加head参数end===================return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.example.demo.controller")).paths(PathSelectors.any()).build().globalOperationParameters(pars) // 添加头部信息.enable(enable);}
