Spring Boot学习教程(三)
4. Spring Boot与Web开发
4.1 使用Spring Boot进行Web的简介
使用Spring Boot进行Web开发的三部曲:
- 创建Spring Boot应用,选中我们需要的模块(Web、数据库等)
- Spring Boot已经默认将这些场景都配置好了,只需要在配置文件中指定少量配置就可以运行了
- 自己编写业务代码
在这个过程中,我们需要去思考的问题有: 这个场景里,Spring Boot帮我们配置了什么? 我们需不需要/能不能进行配置的修改? 如果能修改,需要/能够修改哪些配置? 我们能不能进行配置的扩展?
4.2 Spring Boot对静态资源的映射规则
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {
//可以设置和静态资源资源有关的参数,如:缓存时间等
@Override
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();
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
//配置欢迎页(首页)映射
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
4.2.1 规则1:所有的/webjars/**,都去classpath:/META-INF/resources/webjars/下获取资源
webjars :以jar包的方式引入静态资源
<!--引入jquery-webjar-->
<!--要访问时只需要写webjars下面资源的名称即可-->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>
所以访问路径是:
localhost:8080/webjars/jquery/3.5.1/jquery.js
运行结果:
4.2.2 规则2:"/**":访问当前项目的任何资源,如果没有被处理,则默认去到静态资源路径中获取静态资源:
静态资源路径包括:
"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" "/"当前项目的根路径
举个例子:
4.2.3 规则3:欢迎页:静态资源文件夹下的所有index.html页面(被"/**"映射)
当我们访问 localhost:8080/ 时,Spring Boot会帮我们默认获取 index.html 页面
4.2.4 规则4:favicon.ico:放在静态资源文件夹下
favicon.ico是网页标签页左侧显示的小图标
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>终于能显示图标啦!</title>
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon"/>
</head>
<body>
<h1>晚安!倚欧!</h1>
</body>
</html>
tips:如果浏览器没有显示图标,可以使用ctrl+f5进行刷新(会清空缓存)
4.2.5 规则5:允许在配置文件中修改静态资源访问路径
spring.resources.static-locations=classpath:/hello/,classpath:/taotao/
4.3 模板引擎
4.3.1 什么是模板引擎?
模板引擎是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的文档 就是将模板文件和数据通过模板引擎生成一个HTML代码,如图所示:
Spring Boot推荐使用的模板引擎—— Thymeleaf :
4.3.1.1 引入Thymeleaf
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
4.3.1.2 Thymeleaf的使用&语法
如何使用Thymeleaf模板引擎:
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
//默认前缀
public static final String DEFAULT_SUFFIX = ".html";
//默认后缀
也就是说,只要我们把HTML页面放在classpath:/templates/目录下,thymeleaf就能自动进行渲染
Thymeleaf的相关语法:
- 导入thymeleaf的名称空间
<html lang="en" xmlns:th="http://www.thymeleaf.org">
- 使用thymeleaf语法
<!--success.html-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon"/>
</head>
<body>
<h1>成功!</h1>
<div th:text="${hello}">这是显示欢迎信息</div>
</body>
</html>
- 语法规则
1) th:任意html属性:替换原生属性的值 th:text——改变当前元素中的文本内容
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>成功</h1>
<!--th:text: 讲div中的文本内容设置为指定的值(以${}的形式指定值)-->
<div id="div01" class="myDiv" th:id="${hello}" th:class="${hello}" th:text="${hello}">这是显示欢迎信息</div>
</body>
</html>
运行后查看网页源代码:
th语法汇总:
2) 表达式
# Simple expressions:
Variable Expressions: ${...}
# 获取变量值,在底层是OGNL表达式
# 1. 获取对象的属性、调用方法
# 2. 使用内置的几本对象
#ctx: the context object.
#vars: the context variables.
#locale: the context locale.
#request: (only in Web Contexts) the HttpServletRequest object.
#response: (only in Web Contexts) the HttpServletResponse object.
#session: (only in Web Contexts) the HttpSession object.
#servletContext: (only in Web Contexts) the ServletContext object.
# 3. 内置的工具对象
#execInfo: information about the template being processed.
#messages: methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax.
#uris: methods for escaping parts of URLs/URIs
#conversions: methods for executing the configured conversion service (if any).
#dates: methods for java.util.Date objects: formatting, component extraction, etc.
#calendars: analogous to #dates, but for java.util.Calendar objects.
#numbers: methods for formatting numeric objects.
#strings: methods for String objects: contains, startsWith, prepending/appending, etc.
#objects: methods for objects in general.
#bools: methods for boolean evaluation.
#arrays: methods for arrays.
#lists: methods for lists.
#sets: methods for sets.
#maps: methods for maps.
#aggregates: methods for creating aggregates on arrays or collections.
#ids: methods for dealing with id attributes that might be repeated (for example, as a result of an iteration).
Selection Variable Expressions: *{...}
# 选择表达式:和${}在功能上是一样的,不过有一个补充用法:
配合th:object="${session.user}:"进行使用
Message Expressions: #{...}
# 获取国际化内容
Link URL Expressions: @{...}
# 定义URL
# 举个例子:
# @{/order/process(execId=${execId},execType='FAST')}
Fragment Expressions: ~{...}
# 片段引用表达式
# Literals
# 字面量
Text literals: 'one text', 'Another one!',…
Number literals: 0, 34, 3.0, 12.3,…
Boolean literals: true, false
Null literal: null
Literal tokens: one, sometext, main,…
# Text operations:
# 文本操作
String concatenation: +
Literal substitutions: |The name is ${name}|
# Arithmetic operations:
# 数学运算
Binary operators: +, -, *, /, %
Minus sign (unary operator): -
# Boolean operations:
# 布尔运算
Binary operators: and, or
Boolean negation (unary operator): !, not
# Comparisons and equality:
# 比较运算
Comparators: >, <, >=, <= (gt, lt, ge, le)
Equality operators: ==, != (eq, ne)
# Conditional operators:
# 条件语句
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
# Special tokens:
# 特殊操作符
No-Operation: _
# 不进行操作
- {...}的补充用法(配合th:object="${session.user}:"进行使用)的示例:
<div th:object="${session.user}">
<p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>
<!--上面的代码等价于下面的代码-->
<div>
<p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
</div>
来看看完整的一个代码实例吧!
<!--success.html-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon"/>
</head>
<body>
<h1>成功!</h1>
<div th:text="${hello}">这是显示欢迎信息</div>
<br>
<hr/>
<div th:text="${hello}"></div>
<br>
<div th:utext="${hello}"></div>
<hr/>
<h4 th:text="${user}" th:each="user:${users}"></h4>
<hr/>
<h4>
<span th:each="user:${users}">[[${user}]]</span>
</h4>
</body>
</html>
//HelloController.java
package com.taotao.springboot.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Arrays;
import java.util.Map;
@Controller
public class HelloController {
@ResponseBody
@RequestMapping("/hello")
public String hello(){
return "Hello World!";
}
@RequestMapping("/success")
public String success(Map<String,Object> map){
map.put("hello","<h1>你好</h1>");
map.put("users", Arrays.asList("涛涛","针不戳","!!!"));
return "success";
}
}
运行结果:
4.4 SpringMVC的自动配置原理
4.4.1 SpringMVC autoconfiguration
Spring Boot已经自动配置好了SpringMVC,并提供了大多数场景的自动配置
以下是Spring Boot对SpringMVC的默认配置:
-
自动配置了 ViewResolver (视图解析器:根据方法的返回值得到视图对象(View),视图对象决定如何渲染)
- ContentNegotiatingViewResolver :组合所有的视图解析器
- 如何定制:我们可以自己向容器中添加一个视图解析器,然后ContentNegotiatingViewResolver就会自动地将其组合起来
不信你看:
-
静态资源文件夹路径( webjars )
-
静态首页访问
-
favicon.ico 访问
-
自动注册了 Converter, GenericConverter , 和 Formatter 这些bean
- Converter:转化器(类型转换)
- Formatter:格式化器
@Bean @Override public FormattingConversionService mvcConversionService() { Format format = this.mvcProperties.getFormat(); WebConversionService conversionService = new WebConversionService(new DateTimeFormatters() .dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime())); addFormatters(conversionService); return conversionService; }- 想要自己添加格式化转化器,只需要将其添加到容器中放在
@Override public void addFormatters(FormatterRegistry registry) { ApplicationConversionService.addBeans(registry, this.beanFactory); }public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) { Set<Object> beans = new LinkedHashSet<>(); beans.addAll(beanFactory.getBeansOfType(GenericConverter.class).values()); beans.addAll(beanFactory.getBeansOfType(Converter.class).values()); beans.addAll(beanFactory.getBeansOfType(Printer.class).values()); beans.addAll(beanFactory.getBeansOfType(Parser.class).values()); for (Object bean : beans) { if (bean instanceof GenericConverter) { registry.addConverter((GenericConverter) bean); } else if (bean instanceof Converter) { registry.addConverter((Converter<?, ?>) bean); } else if (bean instanceof Formatter) { registry.addFormatter((Formatter<?>) bean); } else if (bean instanceof Printer) { registry.addPrinter((Printer<?>) bean); } else if (bean instanceof Parser) { registry.addParser((Parser<?>) bean); } } } -
HttpMessageConverter :SpringMVC用来转换Http请求和响应。例如,可以将对象自动转换为JSON或XML。
它是从容器中确定并获取获取所有的HttpMessageConverter的
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.*;
import org.springframework.http.converter.*;
@Configuration(proxyBeanMethods = false)
public class MyConfiguration {
@Bean
public HttpMessageConverters customConverters() {
HttpMessageConverter<?> additional = ...
HttpMessageConverter<?> another = ...
return new HttpMessageConverters(additional, another);
}
}
自己给容器中添加HttpMessageConverter,只需要将自己的组件注册在容器中(@Bean,@Component)
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.*;
import org.springframework.http.converter.*;
@Configuration(proxyBeanMethods = false)
public class MyConfiguration {
@Bean
public HttpMessageConverters customConverters() {
HttpMessageConverter<?> additional = ...
HttpMessageConverter<?> another = ...
return new HttpMessageConverters(additional, another);
}
}
-
Automatic registration of MessageCodesResolver:定义错误代码生成规则
-
ConfigurableWebBindingInitializer bean: Spring MVC使用WebBindingInitializer来为特定请求初始化WebDataBinder 如果创建一个自己的ConfigurableWebBindingInitializer添加到容器中,Spring Boot会自动配置SpringMVC来使用它(替换掉默认的)
org.springframework.boot.autoconfigure.web:web的所有配置场景
4.4.2 扩展SpringMVC
SpringMVC 自带的自动配置是不够用的,所以常常需要我们自己对其进行拓展;拓展SpringMVC的配置:
编写一个配置类 (@Configuration) ,来实现 WebMvcConfigurer 接口,且不能标注 @EnableWebMvc
这样子可以既保留所有的自动配置,也能使用我们自己编写的拓展配置
//使用WebMvcConfigurer可以来扩展SpringMVC的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//浏览器发送 /taotao 请求来到 success
registry.addViewController("/taotao").setViewName("success");
}
}
来稍微看看他的原理:
- WebMvcAutoConfiguration 是 SpringMVC 的自动配置类
- 在做其他自动配置时会导入 @Import(EnableWebMvcConfiguration.class)
@Configuration(proxyBeanMethods = false)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
//从容器中获取所有的WebMvcConfigurer
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
//一个参考实现
//将所有的WebMvcConfiguration相关配置都来一起调用
@Override
protected void addViewControllers(ViewControllerRegistry registry) {
this.configurers.addViewControllers(registry);
}
- 容器中所有的 WebMvcConfiguration 都会一起起作用
- 我们自己的配置类也会被调用
效果:SpringMVC 的自动配置和我们的扩展配置都会起作用
4.4.3 全面接管SpringMVC
假如Spring Boot不需要所有的 SpringMVC 自动配置了,想要所有配置都由我们自己来手动配置,想要所有的 SpringMVC 的自动配置都失效,这就是对 SpringMVC 的全面接管,想要实现对 SpringMVC 的全面接管,我们只要在配置类中添加 @EnableWebMvc 注解即可
@EnableWebMvc
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/taotao").setViewName("success");
registry.addViewController("/").setViewName("login");
registry.addViewController("/index.html").setViewName("login");
registry.addViewController("/main.html").setViewName("dashboard");
}
为什么在配置类中添加 @EnableWebMvc 后自动配置就失效了呢?我们来看看其中的原理:
- @EnableWebMvc核心源码:
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
- DelegatingWebMvcConfiguration类部分源码:
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
- WebMvcAutoConfiguration类部分源码:
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
//当容器中没有这个组件的时候,这个自动配置类才会生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
-
@EnableWebMvc 注解将 WebMvcConfigurationSupport 组件导入了进来
-
导入的 WebMvcConfigurationSupport 只是 SpringMVC 最基本的功能
4.4.4 修改Spring Boot的默认配置
所介绍的修改默认配置的模式是一个统一的模式,不只是适用于web模块;这个模式具体是这样的:
- Spring Boot在自动配置很多组件的时候,先看容器中有没有用户自己配置( @Bean、@Component ),如果有,就用用户所配置的组件,否则就会自动配置 ( @xxxConditional )。如果有些组件可以有多个(比如 ViewResolver ),它就会将用户配置的组件和他自己自动配置的组合起来;
- 在Spring Boot中会有许多的 xxxConfigurer ,帮助我们进行扩展配置
- 在Spring Boot中会有很多的 xxxCustomizer 帮助我们进行定制配置
4.5 RestfulCRUD——实验实战
A.设置默认访问首页
B.国际化
使用Spring Boot实现国际化的步骤:
- 编写国际化配置文件,抽取页面需要显示的国际化消息
- Spring Boot自动配置好了管理国际化资源文件的组件
@Bean
@ConfigurationProperties(prefix = "spring.messages")
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
@Bean
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;
}
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";
- 去页面获取国际化的值 用 #{} 的语法进行赋值
<!--login.html-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Signin Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link href="asserts/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="asserts/css/signin.css" rel="stylesheet">
</head>
<body class="text-center">
<form class="form-signin" action="dashboard.html">
<img class="mb-4" src="asserts/img/bootstrap-solid.svg" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
<label class="sr-only" th:text="#{login.username}">Username</label>
<input type="text" class="form-control" placeholder="Username" th:placeholder="#{login.username}" required="" autofocus="">
<label class="sr-only" th:text="#{login.password}">Password</label>
<input type="password" class="form-control" placeholder="Password" th:placeholder="#{login.password}" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"/> [[#{login.remember}]]
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
<p class="mt-5 mb-3 text-muted">© 2017-2018</p>
<a class="btn btn-sm">中文</a>
<a class="btn btn-sm">English</a>
</form>
</body>
</html>
效果:根据浏览器语言设置的信息实现国际化的语言切换
国际化的升级版:用户可以通过点击链接来切换页面所用的语言来实现国际化
首先了解一下请求头:
升级版国际化的实现原理: locale(区域信息对象) localeResolver(获取区域信息对象)
//WebMvcAutoConfiguration.java
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
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;
}
默认是根据请求头带来的区域信息获取 Locale 进行国际化
//AcceptHeaderLocaleResolver.java
@Override
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = getDefaultLocale();
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);
}
C.登录
模板引擎页面修改后想要它实时生效,我们需要两步:
- 禁用模板引擎的缓存
# application.properties
# 禁用缓存
spring.thymeleaf.cache=false
- 页面修改完成后重新编译(Ctrl+F9)
登录错误消息的显示:
<p style="color: #bd2130" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
当我们刷新页面时,浏览器会出现这样的提示:
为了防止重复提交表单,我们需要进行重定向
if(!StringUtils.isEmpty(username) && "123456".equals(password)){
session.setAttribute("loginUser",username);
return "redirect:/main.html";
}
D.拦截器进行登录检查
拦截器:
/**
* 实现登录检查,没有登录的用户就不能访问后台的主页,也不能对员工进行增删改查*/
public class LoginHandlerInterceptor implements HandlerInterceptor {
//目标方法执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object user = request.getSession().getAttribute("loginUser");
if(user == null){
//未登录,返回登录页面
request.setAttribute("msg","没有权限,请先登录");
request.getRequestDispatcher("index.html").forward(request,response);
return false;
}else{
//已登录,放行请求
return true;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
注册拦截器:
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/taotao").setViewName("success");
registry.addViewController("/").setViewName("login");
registry.addViewController("/index.html").setViewName("login");
registry.addViewController("/main.html").setViewName("dashboard");
}
//注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**").excludePathPatterns(
"/index.html", "/", "/user/login", "/webjars/**", "/asserts/**", "/resources/**");
}
E.CRUD-员工列表
- Restful-CRUD:CRUD要满足Rest风格
普通的CRUD和Restful-CRUD 的对比:
| 操作 | 普通的CRUD(用URI来区分操作) | Restful-CRUD |
|---|---|---|
| 查询 | 发送getEmp请求 | 直接发送emp请求(以GET方式发送) |
| 添加 | 发送addEmp请求 | emp(以POST方式发送) |
| 修改 | 发送updateEmp请求 | emp(以PUT方式发送) |
| 删除 | 发送deleteEmp请求 | emp(以DELETE方式发送) |
- 实验的请求架构
| 操作 | 请求URI | 请求方式 |
|---|---|---|
| 查询所有员工 | emps | GET |
| 查询某一个员工 | emp/1 | GET |
| 来到添加页面 | emp | GET |
| 添加员工 | emp | POST |
| 来到修改页面(查出员工后进行信息回显) | emp/1 | GET |
| 修改员工 | emp | PUT |
| 删除员工 | emp/1 | DELETE |
- 员工列表
thymeleaf公共页面元素抽取
-
- 抽取公共片段
<div th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</div>
-
- 引入公共片段
<!--选择器-->
<div id="copy-section">
© 2011 The Good Thymes Virtual Grocery
</div>
<body>
<div th:insert="~{footer :: #copy-section}"></div>
</body>
<!--模板名-->
<div th:insert="~{footer :: copy}"></div>
~{templatename::selector} :模板名::选择器 ~{templatename::fragmentname} :模板名::片段名
-
- 默认效果 insert 的功能片段的 div 标签中 如果使用 th:insert 等属性进行引入的话,就可以不写 ~{} 不过使用行内写法的话就得写上 ~{},就像这样: [[~{}]]; [(~{})];
三种引入功能片段的th属性: th:insert: 将公共片段整个插入到声明引入的元素中 th:replace: 将声明引入的元素替换为公共片段 th:include: 将被引入的片段的内容包含进这个标签中
<footer th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</footer>
<body>
<div th:insert="footer :: copy"></div>
<div th:replace="footer :: copy"></div>
<div th:include="footer :: copy"></div>
</body>
结果:
<body>
<div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
</div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
<div>
© 2011 The Good Thymes Virtual Grocery
</div>
</body>
引入片段的时候传入参数:
<nav class="col-md-2 d-none d-md-block bg-light sidebar" id="sidebar">
<div class="sidebar-sticky">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link active" th:class="${activeUri=='main.html'?'nav-link active':'nav-link'}" href="#" th:href="@{/main.html}">
<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-home">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
Dashboard <span class="sr-only">(current)</span>
</a>
</li>
</ul>
</div>
</nav>
F.员工添加
添加页面
<form>
<div class="form-group">
<label>LastName</label>
<input type="text" class="form-control" placeholder="zhangsan">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" class="form-control" placeholder="zhangsan@atguigu.com">
</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="1">
<label class="form-check-label">男</label>
</div>
<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>
<div class="form-group">
<label>department</label>
<select class="form-control">
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input type="text" class="form-control" placeholder="zhangsan">
</div>
<button type="submit" class="btn btn-primary">添加</button>
</form>
在添加员工的操作过程中,容易产生一个问题:提交的数据格式不对; 以日期为例,下面三种日期格式都是允许的,但是对于页面来说,我们只允许一种格式的数据能成功提交: 2000-05-04 2000/05/04 2000.05.04 于是,我们就需要对提交的日期数据格式化,SpringMVC需要将页面提交的值转换为我们需要的类型 默认日期是按照类似于2000/05/04的格式显示的,所以我们需要通过类型转换+格式化,来将2000-05-04作为日期的格式
直接修改配置文件的配置
spring.mvc.format.date=yyyy-MM-dd
G.员工修改
修改添加二合一表单
<form th:action="@{/emp}" method="post">
<!--发送put请求修改员工数据-->
<!--1. SpringMVC中配置HiddenHttpMethodFilter;(SpringBoot自动配置好的)
2. 页面创建一个post表单
3. 创建一个input项,name="_method"值就是我们指定的请求方式
-->
<input type="hidden" name="_method" value="put" th:if="${emp!=null}"/>
<input type="hidden" name="id" th:if="${emp!=null}" th:value="${emp.id}"/>
<div class="form-group">
<label>LastName</label>
<input name="lastName" type="text" class="form-control" placeholder="zhangsan" th:value="${emp!=null}?${emp.lastName}">
</div>
<div class="form-group">
<label>Email</label>
<input name="email" type="email" class="form-control" placeholder="zhangsan@atguigu.com" th:value="${emp!=null}?${emp.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="1" th:checked="${emp!=null}?${emp.gender==1}">
<label class="form-check-label">男</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="0" th:checked="${emp!=null}?${emp.gender==0}">
<label class="form-check-label">女</label>
</div>
</div>
<div class="form-group">
<label>department</label>
<!--提交的是部门的id-->
<select class="form-control" name="department.id">
<option th:selected="${emp!=null}?${dept.id == emp.department.id}" th:value="${dept.id}" th:each="dept:${depts}" th:text="${dept.departmentName}">1</option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input name="birth" type="text" class="form-control" placeholder="zhangsan" th:value="${emp!=null}?${#dates.format(emp.birth, 'yyyy-MM-dd')}" th:placeholder="${'2000-05-04'}">
</div>
<button type="submit" class="btn btn-primary" th:text="${emp!=null}?'修改':'添加'">添加</button>
</form>
H.员工删除
员工删除页面
<tr th:each="emp:${emps}">
<td th:text="${emp.id}"></td>
<td>[[${emp.lastName}]]</td>
<td th:text="${emp.email}"></td>
<td th:text="${emp.gender}==0?'女':'男'"></td>
<td th:text="${emp.department.departmentName}"></td>
<td th:text="${#dates.format(emp.birth, 'yyyy-MM-dd HH:mm')}"></td>
<td>
<a class="btn btn-sm btn-primary" th:href="@{/emp/}+${emp.id}">编辑</a>
<button th:attr="del_uri=@{/emp/}+${emp.id}" class="btn btn-sm btn-danger deleteBtn">删除</button>
</td>
</tr>
4.6 错误处理机制
4.6.1 Spring Boot默认的错误处理机制
默认效果:
- 浏览器会返回一个默认的错误页面
浏览器发送请求的请求头:
- 如果是其他客户端进行访问,则默认响应一个json数据
客户端发送请求的请求头:
原理:可以参照ErrorMvcAutoConfiguration(错误处理的自动配置) 它给容器中添加了以下组件:
- DefaultErrorAttributes
//帮我们在页面共享信息
@Override
@Deprecated
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest);
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addPath(errorAttributes, webRequest);
return errorAttributes;
}
- BasicErrorController
//处理默认/error请求
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
//产生html类型的数据;浏览器发送的请求到这个方法处理
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//去哪个页面作为错误页面;modelAndView包含页面地址和页面内容
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
//产生json数据;其他客户端发送的请求到这个方法处理
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
}
public static final String TEXT_HTML_VALUE = "text/html";
- ErrorPageCustomizer
//系统出现错误以后来到error请求进行处理
@Value("${error.path:/error}")
private String path = "/error";
- DefaultErrorViewResolver
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//默认Spring Boot可以去找到一个error/404页面
String errorViewName = "error/" + viewName;
//假如模板引擎可以解析这个页面地址,那么就用模板引擎进行解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
//模板引擎可用的情况下返回到errorViewName指定的视图地址
return new ModelAndView(errorViewName, model);
}
//当模板引擎不可用时,就在静态资源文件夹下找errorViewName对应的页面(error/404.html)
return resolveResource(errorViewName, model);
}
错误处理的流程如下:
一旦系统出现4xx(客户端错误)或5xx(服务器错误)之类的错误, ErrorPageCustomizer 就会生效(定制错误的响应规则),就会来到 /error请求,然后就会被 BasicErrorController 处理;这一步会分为两种情况:
- 响应页面:(去哪个页面是由 DefaultErrorViewResolver 解析得到的)
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
//所有的ErrorViewResolver得到modelAndView
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
- 响应Json数据
4.6.2 如何定制错误响应
- 如何定制错误的页面
-
有模板引擎的情况下: 将错误页面命名为: 错误状态码/html(例:404/html) ,并放在模板引擎文件夹中的 error 文件夹下 当发生此状态码的错误时便显示该对应页面 另外,我们还可以使用 4xx 和 5xx 作为错误页面的文件名来匹配这种类型的所有错误 不过,会优先寻找 精确的状态码.html 文件 页面能获取的信息: timestamp:时间戳 status:状态码 error:错误提示 exception:异常对象 message:异常信息 errors:JSR303数据校验的错误
-
没有模板引擎的情况下:(也就相当于模板引擎找不到这个错误页面) 默认在静态资源文件下找
-
假如模板引擎和静态资源文件下都没有错误页面,那就会默认来到Spring Boot默认的错误提示页面
源码如下:
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (response.isCommitted()) {
String message = getMessage(model);
logger.error(message);
return;
}
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
Object timestamp = model.get("timestamp");
Object message = model.get("message");
Object trace = model.get("trace");
if (response.getContentType() == null) {
response.setContentType(getContentType());
}
builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
.append("<div id='created'>").append(timestamp).append("</div>")
.append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
.append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
if (message != null) {
builder.append("<div>").append(htmlEscape(message)).append("</div>");
}
if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
response.getWriter().append(builder.toString());
}
- 如何定制错误的json数据
- 一、自定义异常处理&返回定制的json数据
@ControllerAdvice
public class MyExceptionHandler {
//浏览器和客户端返回的都是json数据
@ResponseBody
@ExceptionHandler(UserNotExistException.class)
public Map<String, Object> handleException(Exception e){
Map<String,Object> map = new HashMap<>();
map.put("code","user.notexist");
map.put("message",e.getMessage());
return map;
}
}
//没有自适应效果,也就是说浏览器和客户端访问都是json数据,因为这一块被写死了
- 二、转发到 /error 进行自适应响应效果处理
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
//应该传入自己的错误状态码,否则举不会进入定制错误页面的解析流程
/*Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);*/
request.setAttribute("javax.servlet.error.status_code",500);
//这俩的效果没办法显示出来,需要改进
map.put("code","user.notexist");
map.put("message",e.getMessage());
//转发到/error
return "forward:/error";
}
- 三、将我们的定制数据携带出去
出现错误以后,会来到 /error 请求,会被 BasicErrorController 处理,响应出去可以获取的数据是由 getErrorAttributes 得到的(是 AbstractErrorController (实现的是 ErrorController 接口)规定的方法)
方法1:直接编写一个 ErrorController 的实现类(或者直接编写 AbstractErrorController 的子类),放在容器中 方法2:页面上能用的数据(或者json返回的能用的数据)都是通过errorAttributes.getErrorAttributes得到的 容器中 DefaultErrorAttributes() 默认来进行数据处理
自定义 ErrorAttributes :
//向容器中加入我们自己定义的ErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
//返回值的map就是页面和json能获取的所有字段
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> map = super.getErrorAttributes(webRequest, options);
map.put("company","taotao");
//我们的异常处理器携带的数据
Map<String,Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
map.put("ext",ext);
return map;
}
}
最终的效果:响应是自适应的,可以通过定制 ErrorAttributes 改变需要返回的内容
4.7 配置嵌入式Servlet容器
Spring Boot默认使用的是嵌入式的 Servlet 容器(Tomcat)
需要考虑的问题:
-
问题1: 如何定制和修改Servlet容器的相关配置?
- 修改和Server有关的配置(ServerProperties)
server.port=8081 server.context-path=/crud server.tomcat.uri-encoding=UTF-8 //通用的Servlet容器设置 server.xxx //Tomcat的设置 server.tomcat.xxx- 编写一个WebServerFactoryCustomizer:嵌入式Servlet容器的定制器,来修改Servlet容器的配置
@Bean public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryWebServerFactoryCustomizer(){ return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() { @Override public void customize(ConfigurableWebServerFactory factory) { factory.setPort(8083); } }; }
4.7.1 注册Servlet三大组件(Servlet、Filter、Listener)
由于Spring Boot默认是以jar包的方式启动嵌入式的Servlet容器来启东Spring Boot的Web应用,没有web.xml文件
注册三大组件用以下方式:
ServletRegistrationBean
//注册三大组件
@Bean
public ServletRegistrationBean myServlet(){
ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(),"/myServlet");
return registrationBean;
}
FilterRegistrationBean
@Bean
public FilterRegistrationBean myFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new MyFilter());
registrationBean.setUrlPatterns(Arrays.asList("/hello","/myServlet"));
return registrationBean;
}
ServletListenerRegistrationBean
@Bean
public ServletListenerRegistrationBean<MyListener> myListener(){
ServletListenerRegistrationBean<MyListener> registrationBean = new ServletListenerRegistrationBean<>(new MyListener());
return registrationBean;
}
Spring Boot帮我们自动配置SpringMVC的时候,自动注册了SpringMVC的前端控制器(DispatcherServlet)
@Configuration(proxyBeanMethods = false)
@Conditional(DispatcherServletRegistrationCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
@Import(DispatcherServletConfiguration.class)
protected static class DispatcherServletRegistrationConfiguration {
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath());
//默认拦截: / 所有请求(包括静态资源),但是不拦截jsp请求(/*才会拦截jsp请求)
//可以通过配置文件中的spring.mvc.servlet.path来修改SpringMVC前端控制器默认拦截的请求路径
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
}
4.7.2 使用其他Servlet容器
- 问题2: Spring Boot能不能支持其他的Servlet容器?
Jetty(适用于长连接场景) Undertow(不支持jsp) Spring Boot默认支持切换Jetty和Undertow
默认支持: Tomcat(默认使用) Undertow Jetty
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<artifactId>spring-boot-starter-jetty</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>
<dependency>
<artifactId>spring-boot-starter-undertow</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>
4.7.3 嵌入式Servlet容器自动配置原理
EmbeddedWebServerFactoryCustomizerAutoConfiguration:嵌入式的Servlet容器自动配置
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication
@EnableConfigurationProperties(ServerProperties.class)
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {
/**
* Nested configuration if Tomcat is being used.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })//判断当前是否引入了Tomcat依赖
public static class TomcatWebServerFactoryCustomizerConfiguration {
@Bean
public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment,
ServerProperties serverProperties) {
return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
}
}
4.8 使用外置的Servlet容器
4.8.1 操作
嵌入式Servlet容器:(应用打包为可执行的Jar包) 优点:简单便携 缺点:默认不支持JSP,优化&定制会比较复杂
使用外置的Servlet容器:在外部安装一个Tomcat环境,应用以war包的方式打包
步骤:
-
先创建一个War项目
-
将嵌入式的Tomcat指定为provided
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
- 需要编写一个SpringBootServletInitializer的子类,并调用configure方法
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
//传入Spring Boot应用的主程序
return application.sources(SpringBoot04WebJspApplication.class);
}
}
- 启动服务器,就可以开始使用了