SpringBoot搭建Web项目
准备工作
IDEA 设置中下载 Lombok插件
下载web素材 https://www.lanzoui.com/ib1n7sf
新建一个SpringBoot 项目,导入素材到resources目录下。
添加依赖
pom.xml如下:
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
26
27
28<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<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>
</dependencies>
编写实体类
Department
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package top.fulsun.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
* @className: Department
* @Description: 部门实体
* @version: v1.8.0
* @author: Fulsun
* @date: 2020/4/6 11:48
*/
public class Department {
private Integer id;
private String departmentName;
}Employee
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
26
27
28
29
30
31
32package top.fulsun.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* @className: Employee
* @Description: 员工实体
* @version: v1.8.0
* @author: Fulsun
* @date: 2020/4/6 12:10
*/
public class Employee {
private Integer id;
private String name;
private String email;
private Integer gender; //0: man
private Date birth;
private Department department;
public Employee(String name, String email, Integer gender, Date birth, Department department) {
this.name = name;
this.email = email;
this.gender = gender;
this.birth = birth;
this.department = department;
}
}
编写DAO层
DAO(Data Access Object) 数据访问对象是一个面向对象的数据库接口。包名用dao,使用mybatis的时候一般使用mapper。
DepartmentDao
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54package top.fulsun.dao;
import org.springframework.stereotype.Repository;
import top.fulsun.pojo.Department;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* @className: DepartmentDao
* @Description: 部门数据对象
* @version: v1.8.0
* @author: Fulsun
* @date: 2020/4/6 12:15
*/
public class DepartmentDao {
private static Map<Integer, Department> init = null;
static {
init = new HashMap<>();
init.put(1, new Department(1, "销售部"));
init.put(2, new Department(2, "市场部"));
init.put(3, new Department(3, "运营部"));
init.put(4, new Department(4, "开发部"));
init.put(5, new Department(5, "测试部"));
}
/**
* @return
* @Author Fulsun
* @Description // 获取所有的部门
* @version: v1.8.0
* @Date 12:44 2020/4/6
* @Param
**/
public Collection<Department> getAllDeparment() {
return init.values();
}
/**
* @Author Fulsun
* @Description //根据id查找部门
* @version: v1.8.0
* @Date 12:46 2020/4/6
* @Param
* @return
**/
public Department getDeptByID(Integer id) {
return init.get(id);
}
}EmployeeDao
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51package top.fulsun.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import top.fulsun.pojo.Department;
import top.fulsun.pojo.Employee;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @className: EmployeeDao
* @Description: 员工数据对象
* @version: v1.8.0
* @author: Fulsun
* @date: 2020/4/6 18:58
*/
public class EmployeeDao {
private static Map<Integer, Employee> initEmp = null;
private DepartmentDao departmentDao;
static {
initEmp = new HashMap<>();
initEmp.put(1, new Employee("张一", "张一@163.com", 0, new Date(),new Department(1, "销售部")));
initEmp.put(2, new Employee("张二", "张二@163.com", 0, new Date(),new Department(2, "市场部")));
initEmp.put(3, new Employee("张三", "张三@163.com", 0, new Date(),new Department(3, "运营部")));
initEmp.put(4, new Employee("张四", "张四@163.com", 0, new Date(),new Department(4, "开发部")));
initEmp.put(5, new Employee("张五", "张五@163.com", 0, new Date(),new Department(5, "测试部")));
}
//模拟主键
private static Integer pk = 6;
//添加员工
public void addEmployee(Employee employee) {
if (employee.getId() == null) {
employee.setId(pk++);
}
}
//删除员工
public void delEmployee(Integer id) {
if (id != null) {
initEmp.remove(id);
}
}
}
修改首页
引入 thymeleaf
1
2
3
4
5<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>修改HTML页面
命名空间
1
2
<html lang="en" xmlns:th="http://www.thymeleaf.org"></html>修改资源链接
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
26
27
28<!-- Bootstrap core CSS -->
<link href="../static/css/bootstrap.min.css" rel="stylesheet" />
<!-- Custom styles for this template -->
<link href="../static/css/signin.css" rel="stylesheet" />
<script
type="text/javascript"
src="../static/js/jquery-3.2.1.slim.min.js"
></script>
<!-- 修改成如下内容 -->
<!-- Bootstrap core CSS -->
<link
th:href="@{/css/bootstrap.min.css}"
href="../static/css/bootstrap.min.css"
rel="stylesheet"
/>
<!-- Custom styles for this template -->
<link
th:href="@{/css/signin.css}"
href="../static/css/signin.css"
rel="stylesheet"
/>
<script
type="text/javascript"
th:src="@{/js/jquery-3.2.1.slim.min.js}"
src="../static/js/jquery-3.2.1.slim.min.js"
></script>
设置首页访问
可以通过 Controller 进行跳转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package top.fulsun.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @className: IndexController
* @Description: 首页控制层
* @version: v1.8.0
* @author: Fulsun
* @date: 2020/4/7 20:57
*/
public class IndexController {
public String index() {
return "index";
}
}编写配置类,重写
addViewControllers
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package top.fulsun.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @className: IndexConfig
* @Description: 自定义MVC配置
* @version: v1.8.0
* @author: Fulsun
* @date: 2020/4/7 20:40
*/
public class MyWebMvcConfig implements WebMvcConfigurer {
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
}
}
国际化
i18n
(其来源是英文单词 internationalization的首末字符i和n,18为中间的字符数)是“国际化”的简称。语言的描述规则是下面这样的:
1
2
3
4
5
6
7
8
9
10
11# 语言文字种类-扩展语言文字种类-变体(或方言)-使用区域-变体(或方言)-扩展-私有
language-extlang-script-region-variant-extension-privateuse
# 这些字符串对应的值拼接起来可以对应一个准确的语言。为了方便辨识和识别,通常还有约定:
language 全小写,通常两位,新版规范三位,比如:zh
extlang 全小写,三位,表示扩展语言,比如:粤语 yue (这里还有个 macrolanguage 的事情,先不提了)
script 首字母大写,四位,表示变体,比如:中文的 繁体字 Hant 和 简体字 Hans
region 全大写,两位,表示用于地区,比如:都是繁体中文,香港的惯用语与台湾的会有区别
zh-CN 表示用在中国大陆区域的中文
zh-Hans 表示简体中文。
操作
在resources目录下新疆一个i18n文件夹
新建文件:
index.properties
,index_zh_CN.properties
,inedx_en_US.properties
idea下右键Resource Bundle “index”后会有特殊显示,直接输入en_US即可创建配置文件
编写不同语言的配置文件,IDEA支持同时编辑多个文件
- 选中一个文件后,点击底部的Resource Bundle,点击 New Property后,输入loging.tips后选中,右侧会出现编辑框
1
2
3
4
5login.btn=登录
login.password=密码
login.remember=记住我
login.tip=请登录
login.username=用户名1
2
3
4
5login.btn=Sign in
login.password=Password
login.remember=Remember me
login.tip=Please sign in
login.username=Username
配置yaml文件
SpringBoot对国际化有自动配置!这里又涉及到一个类:MessageSourceAutoConfiguration,查看
MessageSourceAutoConfiguration
里面有一个方法,这里发现SpringBoot已经自动配置好了管理我们国际化资源文件的组件 ResourceBundleMessageSource;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 获取 properties 传递过来的值进行判断
public MessageSource messageSource(MessageSourceProperties properties) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
if (StringUtils.hasText(properties.getBasename())) {
// 设置国际化文件的基础名(去掉语言国家代码的)
messageSource.setBasenames(StringUtils
.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
}
if (properties.getEncoding() != null) {
messageSource.setDefaultEncoding(properties.getEncoding().name());
}
messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
Duration cacheDuration = properties.getCacheDuration();
if (cacheDuration != null) {
messageSource.setCacheMillis(cacheDuration.toMillis());
}
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
return messageSource;
}查看配置类
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
26
27
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
//MessageSourceProperties 的每一个属性的值 可以和配置文件中spring.messages.XXX绑定
public class MessageSourceProperties {
/**
* Comma-separated list of basenames (essentially a fully-qualified classpath
* location), each following the ResourceBundle convention with relaxed support for
* slash based locations. If it doesn't contain a package qualifier (such as
* "org.mypackage"), it will be resolved from the classpath root.
*/
private String basename = "messages";
private Charset encoding = StandardCharsets.UTF_8;
private Duration cacheDuration;
private boolean fallbackToSystemLocale = true;
private boolean alwaysUseMessageFormat = false;
private boolean useCodeAsDefaultMessage = false;
}指定默认的语言文件
1
2
3
4
5spring:
thymeleaf:
cache: false
messages:
basename: i18n.index
使用#{}修改页面
Thymeleaf 文字国际化表达式
#{}
允许我们从一个外部文件获取区域文字信息(.properties),用 Key 索引 Value1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<h1 th:text="#{login.tip}">Please sign in</h1>
<label th:text="#{login.username}">Username</label>
<input
type="text"
th:placeholder="#{login.username}"
placeholder="Username"
required=""
autofocus=""
/>
<label th:text="#{login.password}">Password</label>
<input
type="password"
th:placeholder="#{login.password}"
placeholder="Password"
required=""
/>
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me" /> [[#{login.rember}]]
</label>
</div>
<button type="submit" th:text="#{loging.button}">Sign in</button>
国际化组件配置
在Spring中有一个国际化的Locale (区域信息对象);里面有一个叫做LocaleResolver (获取区域信息对象)的解析器!
在webmvc自动配置文件
WebMvcAutoConfiguration
->WebMvcAutoConfigurationAdapter
下查看到SpringBoot默认配置。1
2
3
4
5
6
7
8
9
10
11
12
13
14
public LocaleResolver localeResolver() {
// 容器中没有就自己配,有的话就用用户配置的
if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
}
// 接收头国际化分解
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}AcceptHeaderLocaleResolver 这个类中有一个方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class AcceptHeaderLocaleResolver implements LocaleResolver {
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = getDefaultLocale();
// 默认的就是根据请求头带来的区域信息获取Locale进行国际化
if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
return defaultLocale;
}
Locale requestLocale = request.getLocale();
List<Locale> supportedLocales = getSupportedLocales();
if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {
return requestLocale;
}
Locale supportedLocale = findSupportedLocale(request, supportedLocales);
if (supportedLocale != null) {
return supportedLocale;
}
return (defaultLocale != null ? defaultLocale : requestLocale);
}
}那假如我们现在想点击链接让我们的国际化资源生效,就需要让我们自己的Locale生效!
我们去自己写一个自己的LocaleResolver,可以在链接上携带区域信息!
修改一下前端页面的跳转连接:
1
2
3<!-- 这里传入参数不需要使用 ?使用 (key=value)-->
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
自定义国际化组件
1 | package top.fulsun.component; |
解析器添加到IOC容器中
错误示例
1
2
3
4
5
6
7
8
9package top.fulsun.config;
public class MyWebMvcConfig implements WebMvcConfigurer {
public MyHeaderLocaleResolver myHeaderLocaleResolver(){
return new MyHeaderLocaleResolver();
}
}错误原因
@ConditonalOnMissingBean
是当容器中没有该bean时,springboot自动配置.未指定bean 的名称,默认采用的是 “方法名” + “首字母小写”的配置方式, 这里的的Bean是
localeResolver
,我添加的@Bean的名称并不是localeResolver,因此springboot的配置仍然生效。1
2
3
4
5
6
public LocaleResolver localeResolver() {
//XXXXXXXX
}
解决
- @Bean不仅可以作用在方法上,也可以作用在注解类型上,在运行时提供注册。
- value:name属性的别名,在不需要其他属性时使用,也就是说value 就是默认值
- name:此bean 的名称,或多个名称,主要的bean的名称加别名。
- 如果未指定,则bean的名称是带注解方法的名称。
- 如果指定了,方法的名称就会忽略
- 如果没有其他属性声明的话,bean的名称和别名可通过value属性配置
- @Bean不仅可以作用在方法上,也可以作用在注解类型上,在运行时提供注册。
修改方法名为
localeResolver
1
2
3
4
public MyHeaderLocaleResolver localeResolver() {
return new MyHeaderLocaleResolver();
}指定Bean的名称为
localeResolver
1
2
3
4
5
// @Bean(value = "localeResolver")
public MyHeaderLocaleResolver myHeaderLocaleResolver() {
return new MyHeaderLocaleResolver();
}测试
登录功能
编写登录控制层
代码如下
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
26
27
28
29package top.fulsun.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @program: springbootdemo
* @description: 登录控制层
* @author: Mr.Sun
* @create: 2020-04-11 18:11
* @Version: 1.8
**/
public class LoginController {
public String verifyUserInformation( { String username, String password, Model model)
System.out.println(username+":"+password);
//业务逻辑:从数据库中获取信息验证账户
if (password.equalsIgnoreCase("123456")) {
return "dashboard";
}else {
model.addAttribute("msg", "用户名或密码错误");
}
return "index";
}
}
页面回显响应
前端页面展示响应消息
1
2
3
4
5
6
7
8
9<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">
Please sign in
</h1>
<!-- msg 不为空的时候加载回显信息 -->
<p
style="color: red"
th:if="${not #strings.isEmpty(msg)}"
th:text="${msg}"
></p>
拦截器
重要接口及类介绍
HandlerExecutionChain类
由
HandlerMethod
和Interceptor
集合组成的类,会被HandlerMapping接口的getHandler方法获取。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class HandlerExecutionChain {
private static final Log logger = LogFactory.getLog(HandlerExecutionChain.class);
private final Object handler;
private HandlerInterceptor[] interceptors;
//一般是使用这个拦截器集合
private List<HandlerInterceptor> interceptorList;
private int interceptorIndex = -1;
}
HandlerInterceptor接口
三个方法的执行顺序如下:
preHandler -> Controller -> postHandler -> model渲染-> afterCompletion
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public interface HandlerInterceptor {
//JDK8中可以这样写了:
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView)throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex)throws Exception {
}
}
AbstractHandlerMapping
HandlerMapping的基础抽象类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
implements HandlerMapping, Ordered, BeanNameAware {
private Object defaultHandler;
private UrlPathHelper urlPathHelper = new UrlPathHelper();
private PathMatcher pathMatcher = new AntPathMatcher();
private final List<Object> interceptors = new ArrayList<>();
private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<>();
private CorsConfigurationSource corsConfigurationSource;
private CorsProcessor corsProcessor = new DefaultCorsProcessor();
private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered
private String beanName;
}
AsyncHandlerInterceptor
- 继承HandlerInterceptor的接口,额外提供了afterConcurrentHandlingStarted方法,该方法是用来处理异步请求。
- 当Controller中有异步请求方法的时候会触发该方法。
- 楼主做过测试,异步请求先支持preHandle、然后执行afterConcurrentHandlingStarted。异步线程完成之后执行preHandle、postHandle、afterCompletion。 有兴趣的读者可自行研究。
HandlerInterceptorAdapter
- 实现AsyncHandlerInterceptor接口的抽象类,一般我们使用拦截器的话都会继承这个类。然后复写相应的方法。
WebRequestInterceptor
与HandlerInterceptor接口类似,区别是WebRequestInterceptor的preHandle没有返回值。还有WebRequestInterceptor是针对请求的,接口方法参数中没有response。
1
2
3
4
5
6
7
8
9public interface WebRequestInterceptor {
void preHandle(WebRequest request) throws Exception;
void postHandle(WebRequest request, ModelMap model)throws Exception;
void afterCompletion(WebRequest request, Exception ex)throws Exception;
}AbstractHandlerMapping内部的interceptors是个Object类型集合。处理的时候判断为MappedInterceptor[加入到mappedInterceptors集合中];
HandlerInterceptor、WebRequestInterceptor(适配成WebRequestHandlerInterceptorAdapter)[加入到adaptedInterceptors中]
MappedInterceptor
一个包括includePatterns和excludePatterns字符串集合并带有HandlerInterceptor的类。 很明显,就是对于某些地址做特殊包括和排除的拦截器。
1
2
3
4
5
6
7
8
9
10
11
12
13public final class MappedInterceptor implements HandlerInterceptor {
private final String[] includePatterns;
private final String[] excludePatterns;
private final HandlerInterceptor interceptor;
private PathMatcher pathMatcher;
}
ConversionServiceExposingInterceptor
- 默认的
<annotation-driven/>
标签初始化的时候会初始化ConversionServiceExposingInterceptor这个拦截器,并被当做构造方法的参数来构造MappedInterceptor
。 - 之后会被加入到
AbstractHandlerMapping
的mappedInterceptors
集合中。 - 该拦截器会在每个请求之前往request中丢入
ConversionService
。主要用于spring:eval
标签的使用。
源码分析
首先我们看下拦截器的如何被调用的。内容参考SpringMVC拦截器详解附带源码分析
Web请求被DispatcherServlet截获后,会调用DispatcherServlet的doDispatcher方法。
很明显地看到,在HandlerAdapter处理(945)之前,以及处理完成之后会调用HandlerExecutionChain的方法。
HandlerExecutionChain的applyPreHandle、applyPostHandle、triggerAfterCompletion方法如下:
很明显,就是调用内部实现HandlerInterceptor该接口集合的各个对应方法。
![](6-Web项目搭建案例/16c085daf8bfb9c0d6879f454195213e.jpg) ![](6-Web项目搭建案例/39b3c43ac9ff3f099f41513eafc374da.png)
HandlerExecutionChain的构造过程。
HandlerExecutionChain(916行)是从HandlerMapping接口的getHandler方法获取的。
1
2
3
4
5
6
7
8
9
10
11
12
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}HandlerMapping的基础抽象类AbstractHandlerMapping中getHandler方法如下:
1
protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception;
我们看到,HandlerExecutionChain的拦截器是从AbstractHandlerMapping中的adaptedInterceptors(349行)和mappedInterceptors(354行)属性中获取的。
拦截器的配置
清楚了HandlerExecutionChain的拦截器属性如何构造之后,下面来看下SpringMVC是如何配置拦截器的。
*-dispatcher.xml
配置文件中添加<mvc:interceptors>
配置,这里配置的每个<mvc:interceptor>
都会被解析成MappedInterceptor。1
2
3
4
5
6
7
8<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/login"/>
<mvc:exclude-mapping path="/index"/>
<bean class="package.interceptor.XXInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
自定义的拦截器
为什么拦截器执行preHandle方法返回false之后还是会执行afterCompletion方法,其实我们看下源码就知道了。
afterCompletion
写在finally{}语句中
关于异步请求方面的拦截器以及第二种配置方法(interceptors集合属性可加入继承自HandlerInterceptorAdapter抽象类的类以及实现WebRequestInterceptor接口的类),读者可自行研究。
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
26
27
28
29
30
31
32
33public class LoginInterceptor extends HandlerInterceptorAdapter {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 获得请求路径的uri
String uri = request.getRequestURI();
// 判断路径是登出还是登录验证,是这两者之一的话执行Controller中定义的方法
if(uri.endsWith("/login/auth") || uri.endsWith("/login/out")) {
return true;
}
// 进入登录页面,判断session中是否有key,有的话重定向到首页,否则进入登录界面
if(uri.endsWith("/login/") || uri.endsWith("/login")) {
if(request.getSession() != null && request.getSession().getAttribute("loginUser") != null) {
response.sendRedirect(request.getContextPath() + "/index");
} else {
return true;
}
}
// 其他情况判断session中是否有key,有的话继续用户的操作
if(request.getSession() != null && request.getSession().getAttribute("loginUser") != null) {
return true;
}
// 最后的情况就是进入登录页面
response.sendRedirect(request.getContextPath() + "/login");
return false;
}
}修改登录成功后跳转到main.html,不显示dashboard的真实view名称。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyWebMvcConfig implements WebMvcConfigurer {
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
registry.addViewController("/main.html").setViewName("dashboard");
}
public MyHeaderLocaleResolver myHeaderLocaleResolver() {
return new MyHeaderLocaleResolver();
}
}效果如下
springboot定义拦截器
登录的逻辑如下:
现在直接输入main.html就能够直接访问,显然是不合理的,这里需要添加拦截所有请求。
1
2
3
4
5
6
7
8
9
10
11
12
public String verifyUserInformation( { String username, String password, Model model)
System.out.println(username+":"+password);
//从数据库中获取信息验证账户
if (password.equalsIgnoreCase("123456")) {
//重定向页面
return "redirect:/main.html";
}else {
model.addAttribute("msg", "用户名或密码错误");
}
return "index";
}
登录成功后添加session信息
代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public String verifyUserInformation( String username,
String password,
Model model,HttpSession session) {
System.out.println(username+":"+password);
//从数据库中获取信息验证账户
if (password.equalsIgnoreCase("123456")) {
//需要添加登录信息到session
session.setAttribute("loginUser",username);
return "redirect:/main.html";
}else {
model.addAttribute("msg", "用户名或密码错误");
}
return "index";
}
自定义拦截器
可以在进入controller层之前拦截判断是否符合我们的安全要求;
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49package top.fulsun.config;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Description:
* @version: v1.0.0
* @author: fulsun
* @createDate 2020/4/18 11:27
* @updateUser
* @updateDate 2020/4/18 11:27
*/
public class LoginHandlerInterceptor extends HandlerInterceptorAdapter {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获得请求路径的uri
String uri = request.getRequestURI();
// 判断路径是登出还是登录验证,是这两者之一的话执行Controller中定义的方法
if(uri.endsWith("/user/login") || uri.endsWith("/user/out")) {
return true;
}
// 进入登录页面,判断session中是否有key,有的话重定向到首页,否则进入登录界面
if(uri.endsWith("/user/login/") || uri.endsWith("/user/login")) {
if(request.getSession() != null && request.getSession().getAttribute("loginUser") != null) {
response.sendRedirect(request.getContextPath() + "/");
} else {
return true;
}
}
// 其他情况判断session中是否有key,有的话继续用户的操作
if(request.getSession() != null && request.getSession().getAttribute("loginUser") != null) {
return true;
}
// 最后的情况就是进入登录页面
//response.sendRedirect(request.getContextPath() + "/index.html");
request.getRequestDispatcher("/").forward(request,response);
return false;
}
}
添加拦截器到容器中
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package top.fulsun.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.fulsun.component.MyHeaderLocaleResolver;
/**
* @className: IndexConfig
* @Description: 自定义MVC配置
* @version: v1.8.0
* @author: Fulsun
* @date: 2020/4/7 20:40
*/
public class MyWebMvcConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/","/index.html","/js/*", "/css/*", "/img/*");
}
}这样再次不登录访问main.html的时候就会返回到首页,到此拦截器添加成功。
公共页面抽取
将导航栏抽取放到templatest下的module下,创建navigation.html ,使用
th:fragment="navigation"
标识1
2
3
4
5
6
7
8
9
10
11
12
13
14<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!--导航栏-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="navigation">
<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">welcome [[${session.loginUser}]]
</a>
<input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
<ul class="navbar-nav px-3">
<li class="nav-item text-nowrap">
<a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">注销</a>
</li>
</ul>
</nav>
</html>使用抽取的页面
templatename::selector
前面是模板文件名,后面是选择器module/navigation
为上述公共部分的文件名,navigation
为th:fragment
的值。这样便可以解决公共部分代码的抽取。
th:include
和th:replace
都是加载代码块内容,但是还是有所不同th:include
:保留自己的主标签,不要th:fragment
的主标签。(官方3.0后不推荐)th:replace
: 不要自己的主标签,保留th:fragment
的主标签。th:insert
:保留自己的主标签,保留th:fragment
的主标签。
1
2<!--顶部导航栏-->
<div th:replace="~{module/navigation::navigation}"></div>
导航栏激活
在公共导航栏的class属性添加判断,在对应的页面上引入模块的时候指定参数的值
如:
th:class="${active=='list.html'?'nav-link active':'nav-link'}"
,修改样式,判断active属性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
26
27
28
29
30
31
32<nav
class="col-md-2 d-none d-md-block bg-light sidebar"
th:fragment="sidebar"
>
<li class="nav-item">
<a
class="nav-link"
href="http://getbootstrap.com/docs/4.0/examples/dashboard/#"
th:href="@{/emps}"
th:class="${active=='list.html'?'nav-link active':'nav-link'}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-file"
>
<path
d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"
></path>
<polyline points="13 2 13 9 20 9"></polyline>
</svg>
员工管理
</a>
</li>
</nav>
在员工管理页面引用的使用传递
(active='list.html')
1
2<!--侧边栏-->
<div th:replace="~{module/navigation::sidebar(active='list.html')}"></div>
增删改查编写
接口编写
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private EmployeeDao employeedao;
private DepartmentDao departmentdao;
// 添加&更新员工信息,addEmployee根据employee是否有Id来判断是否是添加
public String updateOrAddEmp(Employee employee) {
System.out.println("save" + employee.toString());
employeedao.addEmployee(employee);
return "redirect:/emps";
}
public String getEmps(Model model) {
Collection<Employee> allEmps = employeedao.getAll();
model.addAttribute("emps", allEmps);
return "list";
}
// 跳转到添加员工页面
public String toAddEmps(Employee employee, Model model,HttpSession session) {
if(session.getAttribute("departments")==null){
Collection<Department> allDeparment = departmentdao.getAllDeparment();
session.setAttribute("departments", allDeparment);
}
return "emp/add";
}
// 修改员工信息
public String toUpdateEmps(int id , Model model, HttpSession session) {
if(session.getAttribute("departments")==null){
Collection<Department> allDeparment = departmentdao.getAllDeparment();
session.setAttribute("departments", allDeparment);
}
Employee employee = employeedao.getDeptById(id);
model.addAttribute("employee", employee);
return "emp/update";
}
// 删除员工
public String todDelEmps(int id) {
employeedao.delEmployee(id);
return "redirect:/emps";
}
查询员工
前端访问/emps
1
2
3
4
5
6<a
th:href="@{/emps}"
th:class="${active=='list.html'?'nav-link active':'nav-link'}"
>
员工管理
</a>页面
th:each
展示数据1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<tbody>
<tr th:each="emp : ${emps}">
<td th:text="${emp.getId()}">1,001</td>
<td>[[${emp.getName()}]]</td>
<td th:text="${emp.getEmail()}">ipsum</td>
<td th:text="${emp.getGender()==0?'男':'女'}">dolor</td>
<td th:text="${#calendars.format(emp.getBirth(),'yyyy/MM/dd')}">sit</td>
<td th:text="${emp.getDepartment().getDepartmentName()}">sit</td>
<td>
<a
class="btn btn-sm btn-primary"
th:href="@{/emps/update/{id}(id=${emp.getId()})}"
>
编辑</a
>
<a
class="btn btn-sm btn-danger"
th:href="@{/emps/del/{id}(id=${emp.getId()})}"
>删除</a
>
</td>
</tr>
</tbody>
添加员工
添加按钮: 跳转到添加员工页面,在跳转之前需要先查询部门信息显示到添加页面
1
2
3<h2>
<a class="btn-success btn btn-sm" th:href="@{/emps/add}">添加员工</a>
</h2>员工添加页面form表单
/emp
的接收参数是Employee employee
,为了能够自动封装成对象,表单的name属性要和实体类同名对于部门选项,value是部门的Id,所以这里对应的department的id属性,为了能够接收到,name要填写
department.id
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61<form th:action="@{/emp}" method="post">
<div class="form-group">
<label>LastName</label>
<input
type="text"
name="name"
class="form-control"
placeholder="姓名"
/>
</div>
<div class="form-group">
<label>Email</label>
<input
type="email"
name="email"
class="form-control"
placeholder="邮箱地址"
/>
</div>
<div class="form-group">
<label>Gender</label><br />
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
name="gender"
value="0"
/>
<label class="form-check-label">男</label>
</div>
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
name="gender"
value="1"
/>
<label class="form-check-label">女</label>
</div>
</div>
<div class="form-group">
<label>department</label>
<select class="form-control" name="department.id">
<option
th:each="dept:${#session.getAttribute('departments')}"
th:text="${dept.getDepartmentName()}"
th:value="${dept.getId()}"
></option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input
type="text"
name="birth"
class="form-control"
placeholder="2000/01/01"
/>
</div>
<button type="submit" class="btn btn-primary">添加</button>
</form>
修改员工信息
员工页面修改按钮,给按钮绑定当前的id
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<tbody>
<tr th:each="emp : ${emps}">
<td>
<a
class="btn btn-sm btn-primary"
th:href="@{/emps/update/{id}(id=${emp.getId()})}"
>
编辑</a
>
<a
class="btn btn-sm btn-danger"
th:href="@{/emps/del/{id}(id=${emp.getId()})}"
>删除</a
>
</td>
</tr>
</tbody>修改操作:需要先获取原来的数据进行回显
text使用
th:value="${employee.id}"
radio使用
th:checked="${employee.gender==0|1}"
option使用:当部门id和员工的id一样的时候才选中
th:selected="${employee.getDepartment().getId() == dept.getId()}"
隐藏域:
<input type="hidden" th:name="id" th:value="${employee.id}">
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68<form th:action="@{/emp}" method="post">
<input type="hidden" th:name="id" th:value="${employee.id}" />
<div class="form-group">
<label>LastName</label>
<input
type="text"
name="name"
class="form-control"
placeholder="姓名"
th:value="${employee.name}"
/>
</div>
<div class="form-group">
<label>Email</label>
<input
type="email"
name="email"
class="form-control"
placeholder="邮箱地址"
th:value="${employee.email}"
/>
</div>
<div class="form-group">
<label>Gender</label><br />
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
name="gender"
value="0"
th:checked="${employee.gender==0}"
/>
<label class="form-check-label">男</label>
</div>
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="radio"
name="gender"
value="1"
th:checked="${employee.gender==1}"
/>
<label class="form-check-label">女</label>
</div>
</div>
<div class="form-group">
<label>department</label>
<select class="form-control" name="department.id">
<option
th:each="dept:${#session.getAttribute('departments')}"
th:text="${dept.getDepartmentName()}"
th:value="${dept.getId()}"
th:selected="${employee.getDepartment().getId() == dept.getId()}"
></option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input
type="text"
name="birth"
class="form-control"
placeholder="2000/01/01"
th:value="${#calendars.format(employee.birth,'yyyy/MM/dd')}"
/>
</div>
<button type="submit" class="btn btn-primary">添加</button>
</form>
删除员工
- 根据接收的员工id,调用删除方法后,重新获取数据回到列表页面
错误页面解析
DefaultErrorAttributes:
org.springframework.boot.web.servlet.error.DefaultErrorAttributes
,类里为错误情况共享很多错误信息,如。1
2
3
4
5
6
7
8
9
10
11
12
13
14//时间戳
errorAttributes.put("timestamp", new Date());
//状态码
errorAttributes.put("status", status);
// 错误提示
errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
// JSR303数据校验的错误都在这里
errorAttributes.put("errors", result.getAllErrors());
// 异常对象
errorAttributes.put("exception", error.getClass().getName());
// 异常消息
errorAttributes.put("message", error.getMessage());
errorAttributes.put("trace", stackTrace.toString());
errorAttributes.put("path", path);
BasicErrorController:
处理默认配置的/error请求,其中可以产生html文件数据(浏览器请求)和json数据(客户端:例如postMan),
区分方式:浏览器发送请求,其中请求头会优先接受html文件(postMan无设置优先接受数据)
直接追踪
BasicErrorController
的源码内容可以发现下面的一段代码。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
26
27
28
29// org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
// 定义请求路径,如果没有error.path路径,则路径为/error
public class BasicErrorController extends AbstractErrorController {
// 如果支持的格式 text/html
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
// 获取要返回的值
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
// 解析错误视图信息,也就是下面conventionErrorViewResolver中的逻辑
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
// 返回视图,如果没有存在的页面模版,则使用默认错误视图模版
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
// 如果是接受所有格式的HTTP请求
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
// 响应HttpEntity
return new ResponseEntity<>(body, status);
}
}由上可知,
basicErrorControll
用于返回请求错误的controller
类,并能根据HTTP请求可接受的格式不同返回对应的信息,所以在使用浏览器和接口测试工具测试时返回结果存在差异。
ErrorPageCustomizer
当遇到错误时,如果没有自定义
error.path
属性,则请求转发至/error
.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
26
27
28//org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration.ErrorPageCustomizer
/**
* {@link WebServerFactoryCustomizer} that configures the server's error pages.
*/
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
private final ServerProperties properties;
private final DispatcherServletPath dispatcherServletPath;
protected ErrorPageCustomizer(ServerProperties properties,
DispatcherServletPath dispatcherServletPath) {
this.properties = properties;
this.dispatcherServletPath = dispatcherServletPath;
}
// 注册错误页面
// this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath())
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
//getPath()得到如下地址,如果没有自定义error.path属性,则去/error位置
//@Value("${error.path:/error}")
//private String path = "/error";
ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath
.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
public int getOrder() {
return 0;
}
}
conventionErrorViewResolver
一步步深入查看 SpringBoot 的默认错误处理实现,查看
conventionErrorViewResolver
方法。下面是 DefaultErrorViewResolver 类的部分代码。- 从而我们可以得知,错误页面首先会检查
模版引擎
文件夹下的/error/HTTP状态码
文件 - 如果不存在,则检查去模版引擎下的
/error/4xx
或者/error/5xx
文件 - 如果还不存在,则检查
静态资源
文件夹下对应的上述文件。
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46// org.springframework.boot.autoconfigure.web.servlet.error.DefaultErrorViewResolver
// 初始化参数,key 是HTTP状态码第一位。
static {
Map<Series, String> views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
// 使用HTTP完整状态码检查是否有页面可以匹配
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
// 使用 HTTP 状态码第一位匹配初始化中的参数创建视图对象
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
// 拼接错误视图路径 /eroor/[viewname]
String errorViewName = "error/" + viewName;
// 使用模版引擎尝试创建视图对象
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
// 没有模版引擎,使用静态资源文件夹解析视图
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
// 遍历静态资源文件夹,检查是否有存在视图
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}- 从而我们可以得知,错误页面首先会检查
而 Thymeleaf 对于错误页面的解析实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider
public class ThymeleafTemplateAvailabilityProvider
implements TemplateAvailabilityProvider {
public boolean isTemplateAvailable(String view, Environment environment,
ClassLoader classLoader, ResourceLoader resourceLoader) {
if (ClassUtils.isPresent("org.thymeleaf.spring5.SpringTemplateEngine",
classLoader)) {
String prefix = environment.getProperty("spring.thymeleaf.prefix",
ThymeleafProperties.DEFAULT_PREFIX);
String suffix = environment.getProperty("spring.thymeleaf.suffix",
ThymeleafProperties.DEFAULT_SUFFIX);
return resourceLoader.getResource(prefix + view + suffix).exists();
}
return false;
}
}
自定义异常页面
- 经过上面的 SpringBoot 错误机制源码分析,知道当遇到4xx或者5xx之类的错误情况,SpringBoot 会首先返回到
模版引擎
文件夹下的/error/HTTP
状态码 文件,如果不存在,则检查去模版引擎下的/error/4xx
或者/error/5xx
文件,如果还不存在,则检查静态资源
文件夹下对应的上述文件。并且在返回时会共享一些错误信息,这些错误信息可以在模版引擎中直接使用。 - 因此,需要自定义错误页面,只需要在模版文件夹下的 error 文件夹下创建4xx 或者 5xx 文件即可。
resources/templates/error/4xx.html | 5xx.html
(或者具体的404)
错误页
页面如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>[[${status}]]</title>
<!-- Bootstrap core CSS -->
<link href="/webjars/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet">
</head>
<body >
<div class="m-5" >
<p>错误码:[[${status}]]</p>
<p >信息:[[${message}]]</p>
<p >时间:[[${#dates.format(timestamp,'yyyy-MM-dd hh:mm:ss ')}]]</p>
<p >请求路径:[[${path}]]</p>
</div>
</body>
</html>
自定义错误内容
根据上面的 SpringBoot 错误处理原理分析,得知最终返回的 JSON 信息是从一个 map 对象中转换出来的,那么,只要能自定义 map 中的值,就可以自定义错误信息的 json 格式了。
直接重写
DefaultErrorAttributes
类的getErrorAttributes
方法即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义错误信息JSON值
*/
public class ErrorAttributesCustom extends DefaultErrorAttributes {
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
String code = map.get("status").toString();
String message = map.get("error").toString();
HashMap<String, Object> hashMap = new HashMap<>();
//返回自定义的数据
hashMap.put("code", code);
hashMap.put("message", message);
return hashMap;
}
}
统一异常处理
使用
@ControllerAdvice
结合@ExceptionHandler
注解可以实现统一的异常处理,@ExceptionHandler
注解的类会自动应用在每一个被@RequestMapping
注解的方法。当程序中出现异常时会层层上抛错误被@ControllerAdvice捕获.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import 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;
import javax.servlet.http.HttpServletRequest;
/**
* <p>
* 统一的异常处理
*/
public class ExceptionHandle {
public Response handleException(Exception e) {
log.info("异常 {}", e);
if (e instanceof BaseException) {
BaseException exception = (BaseException) e;
String code = exception.getCode();
String message = exception.getMessage();
return ResponseUtill.error(code, message);
}
return ResponseUtill.error(ResponseEnum.UNKNOW_ERROR);
}
}请求异常页面得到响应如下。
1
2
3
4
5{
"code": "-1",
"data": [],
"message": "未知错误"
}
静态资源处理
Spring boot使用起步依赖和自动配置极大的减少了开发量,但其内部使用的还是SpringMVC处理HTTP请求,处理过程如下:
- 首先找到该请求对应的
HttpRequestHandler
,对于静态资源请求,是ResourceHttpRequestHandler
- 依次查找各静态资源根目录,获取url对应的Resource(即获取与url路径一致的文件)
- 首先找到该请求对应的
静态资源的自动配置
WebMvcAutoConfiguration > WebMvcAutoConfigurationAdapter > addResourceHandlers
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
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//如果自定义的了配置,则默认的失效
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
//注册一个 /webjars/** 资源,对应的路径 classpath:/META-INF/resources/webjars/
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
//映射资源路径
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
// 静态资源目录匹配
// private String staticPathPattern = "/**";
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}通过分析,匹配不上的
/**
对应的本地资源路径的映射查看getStaticLocations,得到下面对应的 4 个路径1
2
3
4
5
6
7
8
9
10
11
12
13
14
15private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
public String[] getStaticLocations() {
return this.staticLocations;
}
//=======
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
};
//=======除此之外,还增加了一个
locations.add(new ClassPathResource("/"))因此spring boot默认静态资源根目录共有5个:
- classpath:/META-INF/resources/
- classpath:/resources/
- classpath:/static/
- classpath:/public/
- classpath:/
1 | # 默认创建的 |
webjars
- 对于日常的web开发而言,像css、js、images、font等静态资源文件管理是非常的混乱的、比如jQuery、Bootstrap、Vue.js等,可能每个框架使用的版本都不一样、一不注意就会出现版本冲突或者重复添加的问题。
- WebJars是将客户端(浏览器)资源(JavaScript,Css等)打成jar包文件,以对资源进行统一依赖管理。将这些通用的Web前端资源打包成Java的Jar包,然后借助Maven工具对其管理,保证这些Web资源版本唯一性,升级也比较容易,WebJars的jar包部署在Maven中央仓库上。
- 关于webjars资源,有一个专门的网站http://www.webjars.org/,我们可以到这个**网站上找到自己需要的资源,在自己的工程中添加入maven依赖,即可直接使用这些资源了**。
使用
首先在 WebJars官网 找到项目所需的依赖,例如在
pom.xml
引入 jQuery、BootStrap前端组件等。例如:
1
2
3
4
5<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.4.1</version>
</dependency>浏览器输入
/webjars/**
对应的是classpath:/META-INF/resources/webjars/**
静态资源目录优先级
通过对自动配置进行分析得到下面的静态资源目录,优先级从高到低
数组中的值 在项目中的位置 classpath:/META-INF/resources/ src/main/resources/META-INF/resources/ classpath:/resources/ src/main/resources/resources/ classpath:/static/ src/main/resources/static/ classpath:/public/ src/main/resources/public/ 在 public , static , resources下创建文件
1.js
内容对应 Hello_public,Hello_static,Hello_resources.
去掉 resources 下的文件
静态资源处理方式
- Webjars方式 ==
localhost:8080/webjars/**
- classpath下的 pulic,static,resources 文件夹 ==
localhost:8080/**
- Webjars方式 ==
自定义静态资源访问
- 在Springboot中默认的静态资源路径有:
classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/
- 从这里可以看出这里的静态资源路径都是在classpath中(也就是在项目路径下指定的这几个文件夹)
- 试想这样一种情况:一个网站有文件上传文件的功能,如果被上传的文件放在上述的那些文件夹中会有怎样的后果?
- 网站数据与程序代码不能有效分离;
- 当项目被打包成一个.jar文件部署时,再将上传的文件放到这个.jar文件中是有多么低的效率;
- 网站数据的备份将会很痛苦。
- 此时可能最佳的解决办法是将静态资源路径设置到磁盘的基本个目录。
编写配置类
映射/upload请求对应的资源路径
1
2
3
4
5
6
7
8
9
10
11
12
13**
* 自定义静态资源访问
*/
public class WebMvcConfig extends WebMvcConfigurerAdapter {
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//将所有到 /upload/** 访问都映射路径 D:/upload/ 下
registry.addResourceHandler("/upload/**")
.addResourceLocations("file:D:/upload/");
}
}
修改配置文件
spring.resources.static-locations
:这里的配置是覆盖默认配置,所以需要将默认的也加上,否则static、public等这些路径将不能被当作静态资源路径。配置项见官方文档直接修改
ResourceHttpRequestHandler
的bean定义(虽然也可以达到目的,但有点画蛇添足,建议使用这一种方式)配置文件修改如下:
- 在这个最末尾的file:${web.upload-path}之所有要加file:是因为指定的是一个具体的硬盘路径
- 其他的使用classpath指的是系统环境变量
1
2
3
4
5
6
7# 自定义的属性,指定了一个路径,注意要以/结尾;
web.upload-path=D:/upload/
# 表示所有的访问都经过静态资源路径;
spring.mvc.static-path-pattern=/**
# 在这里配置静态资源路径
spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,\
classpath:/static/,classpath:/public/,file:${web.upload-path}通过在application.yaml中配置
spring.resources.static-locations
定制首页
WebMvcAutoConfiguration -> EnableWebMvcConfiguration下有如下二个方法
1
2
3
4
5
6
7
8
9
10avprivate Optional<Resource> getWelcomePage() {
//静态资源路径
String[] locations = WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations());
return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}
private Resource getIndexHtml(String location) {
//首页取得文件是 静态资源路径下的 index.html 文件
return this.resourceLoader.getResource(location + "index.html");
}welcome页面就是在静态资源目录下的index.html文件
在 static 文件夹下创建 index.html,输入首页.访问
127.0.0.1:8080/
定义 favicon.ico
Springboot 在 2.2.+ 版本移除了对图标的配置
在2.2.x以前的版本,使用需要关闭默认的图标.
spring.mvc.favicon.enabled=false
,然后直接将你需要的favicon.ico文件存放在static下面就可以。但到了2.2.X以后的版本,去掉了默认的自动配置,需要我们手动在每一个页面添加自己网站的Favicon图标。
1
2
3
4
5<!-- 在 static 下放下图标 -->
<link rel="icon" href="favicon.ico" type="image/x-icon" />
<!-- 如果使用thymeleaf模板引擎,这里放在 resources/static/asserts/ 静态资源文件夹下: -->
<link rel="shortcut icon" th:href="/@{/asserts/favicon.ico}" />
日志显示请求路径列表
日志接口信息是由
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
类在启动的时候,通过扫描Spring MVC的@Controller
、@RequestMapping
等注解去发现应用提供的所有接口信息。然后在日志中打印,以方便开发者排查关于接口相关的启动是否正确。从Spring Boot 2.1.0版本开始,就不再打印这些信息了,完整的启动日志变的非常少
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
162021-06-11 13:45:32.958 INFO 2440 --- [ restartedMain] tk.fulsun.demo.SwaggerApplication : Starting SwaggerApplication on CN5CG8392ZRF with PID 2440 (C:\Users\fulsun\IdeaProjects\springboot-study\demo-swagger\target\classes started by fulsun in C:\Users\fulsun\IdeaProjects\springboot-study)
2021-06-11 13:45:32.966 INFO 2440 --- [ restartedMain] tk.fulsun.demo.SwaggerApplication : The following profiles are active: default
2021-06-11 13:45:33.214 INFO 2440 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2021-06-11 13:45:33.214 INFO 2440 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2021-06-11 13:45:38.775 INFO 2440 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2021-06-11 13:45:38.823 INFO 2440 --- [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2021-06-11 13:45:38.823 INFO 2440 --- [ restartedMain] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.46]
2021-06-11 13:45:39.202 INFO 2440 --- [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2021-06-11 13:45:39.202 INFO 2440 --- [ restartedMain] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 5988 ms
2021-06-11 13:45:41.356 INFO 2440 --- [ restartedMain] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2021-06-11 13:45:41.437 INFO 2440 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
2021-06-11 13:45:41.950 INFO 2440 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2021-06-11 13:45:42.992 WARN 2440 --- [ restartedMain] d.s.r.o.OperationImplicitParameterReader : Unable to interpret the implicit parameter configuration with dataType: , dataTypeClass: class java.lang.Void
2021-06-11 13:45:43.008 WARN 2440 --- [ restartedMain] d.s.r.o.OperationImplicitParameterReader : Unable to interpret the implicit parameter configuration with dataType: , dataTypeClass: class java.lang.Void
2021-06-11 13:45:43.055 INFO 2440 --- [ restartedMain] tk.fulsun.demo.SwaggerApplication : Started SwaggerApplication in 12.067 seconds (JVM running for 14.218)为什么在Spring Boot 2.1.x版本中不再打印请求路径列表呢?
主要是由于从该版本开始,将这些日志的打印级别做了调整:从原来的
INFO
调整为TRACE
。所以,当我们希望在应用启动的时候打印这些信息的话,只需要在配置文件增增加对RequestMappingHandlerMapping
类的打印级别设置即可,比如在application.properties
中增加下面这行配置:1
2
3
4
5
6logging.level.org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping=trace
---
logging:
level:
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: trace
在增加了上面的配置之后重启应用,便可以看到如下的日志打印:
1
2
3
4
5
6
7
8
9
10
112021-06-11 13:50:27.013 TRACE 11836 --- [ restartedMain] s.w.s.m.m.a.RequestMappingHandlerMapping :
t.f.d.c.StudentController:
{GET /student/list}: bbb()
{POST /student/aaa}: aaa(User)
{GET /student/his-teachers}: ccc()
2021-06-11 13:50:27.013 TRACE 11836 --- [ restartedMain] s.w.s.m.m.a.RequestMappingHandlerMapping :
t.f.d.c.TeacherController:
{GET /teacher/xxx}: xxx()
2021-06-11 13:50:27.013 TRACE 11836 --- [ restartedMain] s.w.s.m.m.a.RequestMappingHandlerMapping :
t.f.d.c.TestController:
{GET /api/test}: test()可以看到在2.1.x版本之后,除了调整了日志级别之外,对于打印内容也做了调整。现在的打印内容根据接口创建的Controller类做了分类打印,这样更有助于开发者根据自己编写的Controller来查找初始化了那些HTTP接口。