Spring Boot笔记-Spring Boot与Web开发(四)

1.简介

使用Spring Boot进行Web应用的开发:

  1. 创建Spring Boot应用,选中需要的模块
  2. Spring Boot已经默认将这些场景配置好了,只需要在配置文件中指定少量配置就可以运行起来
  3. 编写业务代码

自动配置原理:

  • xxxAutoConfiguration帮助我们在容器中自动配置组件
  • xxxProperties用来将配置文件中的内容进行封装

2.Spring Boot对静态资源的映射规则

打开WebMVCAutoConfiguration类,找到它的内部类WebMVCAutoConfigurationAdapter里的addResourceHandlers()方法。

public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
    } else {
        Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
        CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
        if (!registry.hasMappingForPattern("/webjars/**")) {
            this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
        }
        String staticPathPattern = this.mvcProperties.getStaticPathPattern();
        if (!registry.hasMappingForPattern(staticPathPattern)) {
            this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
        }
    }
}

说到这段代码,不得不说一个东西:webjars,我们访问官方网站:https://www.webjars.org/。webjars的目的是以jar包的形式引入一些静态资源。比如我需要引入jquery,就可以将jquery的坐标放到pom.xml中即可。

1.WebJars里访问

所有的/webjars/**,都去classpath:/META-INF/resources/webjars/下寻找资源,引入jquery的jar包后,看一下里面目录结构,如果和我的不一致,可以将Compact Middle Packages的勾选去掉。

此时,如果我希望访问jquery.js,可以通过浏览器请求localhost:8080/webjars/jquery/3.4.1/jquery.js即可。

2./**路径的访问

“/**”访问当前项目的任何资源,会去静态资源的文件夹查找("/"表示当前项目的根路径)。

静态资源文件夹:"classpath:/META‐INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"。

发送一个请求,如果没有controller处理,就会请求到静态资源文件夹中,去里面查看有没有。

在static目录下,添加一个静态文件,比方说我添加了一个test.html,通过浏览器访问:localhost:8080/test.html。因为test.html没有controller来处理,此时,请求就发送到了静态资源文件夹里,在静态资源文件夹里,找到了test.html,就给我们展示了出来。

3.欢迎页的访问

当浏览器访问localhost:8080/的时候,因为/也符合/**,所以请求到了静态资源目录,会去静态资源目录查找index页面。

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
    WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
    welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
    return welcomePageHandlerMapping;
}
private Optional<Resource> getWelcomePage() {
    String[] locations = WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations());
    return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}

找到WebMVCAutoConfiguration.class中的welcomePageHandlerMapping()方法,这个方法是用来处理欢迎页请求的。观察new WelcomePageHandlerMapping()方法里倒数第二个参数,会调用getWelcomePage()方法,这里也贴出来了。再看locations变量的值,它是从resourceProperties.getStaticLocations()获取的,点进去一看究竟,方法体直接返回了this.staticLocations,再找staticLocations的赋值,发现ResourceProperties()构造函数中,将CLASSPATH_RESOURCES_LOCATIONS的值赋给了它,而CLASSPATH_RESOURCES_LOCATIONS的值就是上面说的静态资源文件夹了。

WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
    if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
        logger.info("Adding welcome page: " + welcomePage.get());
        this.setRootViewName("forward:index.html");
    } else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
        logger.info("Adding welcome page template: index");
        this.setRootViewName("index");
    }
}

于是,当访问/的时候,就会去静态资源文件夹下查找index.html,也就是上面的这些代码。

4.icon的访问

因为使用的Spring Boot版本问题,在WebMvcAutoConfigruation中找不到关于请求favicon的请求处理了,同样它也会去静态资源文件夹查找favicon.ico的文件。如果可以找到,就显示这个图标。

于是,可以将一个icon文件放到静态资源文件下查看效果。如果图标不出现,可以使用Ctrl+F5对浏览器进行强制刷新,就可以看到效果了。

最后再说一句,如果手动设置静态资源的文件目录,可以在配置文件中指定,value如果有多值,使用英文逗号分开即可。当手动配置上了静态资源,就不会从默认的静态资源查找了。

spring.resources.static-locations=classpath:/hello/,classpath:/world/

3.模板引擎

Spring Boot使用的是内置的Tomcat,不支持JSP,如果纯粹些静态页面的话,给开发带来很大麻烦。于是Spring Boot推出了Thymeleaf模板帮助我们解决这个问题,Thymeleaf语法更简单,功能更强大。

1.引入Thymeleaf

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

2.Thymeleaf的使用

通过ThymeleafProperties可以知道,如果我们将HTML代码放在classpath:/templates/下,Thymeleaf引擎就会识别到并进行渲染。

public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";

添加一个处理“/success”请求的方法,返回“success”,并在template里加入success.html代码。

@RequestMapping("/success")
public String success() {
    // 因为没有带@ResponseBody注解,所以返回的是页面,因为这里使用了Thymeleaf
    // 于是Thymeleaf会去classpath:/templates里查找,也就是找classpath:/template/success.html
    return "success";
}

Thymeleaf文档地址:https://www.thymeleaf.org/documentation.html,根据自己的版本选择文档的版本,这里我选择3.0版本:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html

新写一个处理“/showParam”的方法,返回“showParam”,同样,在template里加入showParam.html文件。

@RequestMapping("/showParam")
public String showParam(Map<String, Object> map) {
    map.put("key", "value");
    map.put("users", Arrays.asList("zhangsan", "lisi", "wangwu"));
    return "showParam";
}

1.导入Thymeleaf的名称空间,也就是在html标签上加上一小段代码,用于Thymeleaf的代码提示功能。

<html lang="en" xmlns:th="http://www.thymeleaf.org">

2.使用Thymeleaf语法,在html页面取到map中的值并展示。

<body>
<!--th:text 将div里的文本设置成${key}的值-->
<div th:text="${key}"></div>
<h1 th:text="${user}" th:each="user:${users}"></h1>
</body>

3.语法规则

1.th:任意html属性,可以替换原生属性的值

OrderFeatureAttributes
1Fragment inclusion(片段包含)

th:insert

th:replace

2Fragment iteration(片段迭代)th:each
3Conditional evaluation(条件判断)

th:if

th:unless

th:switch

th:case

4Local variable definition(局部变量定义)

th:object

th:with

5General attribute modification(常规属性修改)

th:attr

th:attrprepend

th:attrappend

6Specific attribute modification(特定属性修改)

th:value

th:href

th:src

……

7Text (tag body modification)(修改标签体内容)

th:text

th:utext

8Fragment specification(声明片段)th:fragment
9Fragment removal(移除片段)th:remove

2.表达式

这段内容有点多,具体的可以参考官方文档中标准表达式语法这一节:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#standard-expression-syntax

4.Spring MVC自动配置

官网文档地址:https://docs.spring.io/spring-boot/docs/2.2.6.RELEASE/reference/htmlsingle/#boot-features-developing-web-applications

1.Spring MVC auto configuration

Spring Boot为Spring MVC提供了自动配置,自动配置在Spring的默认值之上添加了以下功能:

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
    • 自动配置了视图解析器(viewResolver),根据方法的返回值,得到视图对象,视图对象决定如何渲染。
    • ContentNegotiatingViewResolver获取到所有的视图。
    • 自己定制视图解析器,给容器添加一个视图解析器,会自动将其组合起来。
  • Support for serving static resources, including support for WebJars (covered later in this document)).
    • 静态资源文件夹和WebJars相关内容。
  • Automatic registration of Converter, GenericConverter, and Formatter beans.
    • 如果需要自己添加Converter或Formatter,只需要放在容器中就好,Spring Boot会帮我们加载到容器中,类似自定义ViewResolver。
  • Support for HttpMessageConverters (covered later in this document).
    • HttpMessageConverters是Spring MVC用来转换Http请求和响应的,同样支持自定义,只需要放在容器中即可。
  • Automatic registration of MessageCodesResolver (covered later in this document).
    • 定义错误代码生成规则。
  • Static index.html support.
    • 静态index.html欢迎页的访问。
  • Custom Favicon support (covered later in this document).
    • 自定义图标访问。
  • Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
    • 初始化Web数据绑定器,也可以自定义。
@Bean
public ViewResolver getMyViewResolver() {
    return new MyViewResolver();
}
// 定义一个视图解析器,在项目启动的时候,会自动加载进来
private static class MyViewResolver implements ViewResolver {
    @Override
    public View resolveViewName(String s, Locale locale) throws Exception {
        return null;
    }
}

下面这句话和视频里讲解的有点不一样,因为我使用的版本是2.x的,视频里用的1.x的。

如果希望保持Spring Boot对Spring MVC自动配置的功能,并且只是想添加一些MVC的功能(Interceptor、Formatter、ViewController等),可以自己添加一个带有@Configuration注解的类。这个类的类型是WebMvcConfigurer,并且不能带有@EnableWebMvc注解。如果想要完全接管Spring MVC,可以添加@Configuration和@EnableWebMvc注解,也就是全面接管Spring MVC了。

2.扩展Spring MVC

创建自己的MVC配置类,实现WebMvcConfigurer接口,带上@Configuration注解,重写addViewControllers()方法,这种方式既保留了所有的自动配置,也扩展了我们的配置。

package com.atguigu.springboot.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 浏览器发送/atguigu请求,返回success页面
        registry.addViewController("/atguigu").setViewName("success");
    }
}

原理:

1.WebMVCAutoConfiguration是Spring MVC的自动配置类。

2.找到WebMvcAutoConfigurationAdapter类,它也是实现了WebMvcConfigurer接口,类名上面有@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})的注解,点开EnableWebMvcConfiguration类 ,这个类继承了DelegatingWebMvcConfiguration类,点开这个类,可以发现下面这段代码,这段代码的意思是从容器中获取所有的WebMvcConfigurer类。找到这个类里的addViewControllers()方法,它调用的是configuers的addViewControllers()方法。点进去查看,可以发现它把所有的WebMvcConfigurer的配置都调用了一遍,也就是所有的WebMvcConfigurer都起作用的意思。

// DelegatingWebMvcConfiguration类
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
    if (!CollectionUtils.isEmpty(configurers)) {
        this.configurers.addWebMvcConfigurers(configurers);
    }
}

// DelegatingWebMvcConfiguration类的addViewControllers()方法具体实现
public void addViewControllers(ViewControllerRegistry registry) {
    Iterator var2 = this.delegates.iterator();
    while(var2.hasNext()) {
        WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
        delegate.addViewControllers(registry);
    }
}

3.容器中所有的WebMvcConfigurer都会起作用。

实现的效果就是:Spring MVC的自动配置和我们的扩展配置都会起作用。

3.全面接管Spring MVC

只需要在配置类上添加@EnableWebMvc注解,Spring Boot对Spring MVC的自动配置都不会执行了,所有的都是我们自己来配置,所有的Spring MVC的自动配置都失效了。

原理:

1.@EnableWebMvc的核心

@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}

2. 点开DelegatingWebMvcConfiguration类

@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
}

3.找到WebMvcAutoConfiguration类

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
}

根据注解@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})的意思,当容器中没有WebMvcConfigurationSupport.class的时候,WebMvcAutoConfiguration才会生效。但是@EnableWebMvc注解上有一个@Import({DelegatingWebMvcConfiguration.class}),并且DelegatingWebMvcConfiguration是WebMvcConfigurationSupport的子类,于是WebMvcConfigurationSupport也被导入了,所以@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})的值就是false了,因此原有的自动配置都失效了。

4.@EnableWebMvc将WebMvcConfigurationSupport导入到容器中了。

5.导入的WebMvcConfigurationSupport是Spring MVC最基本的功能,因此其他自动配置失效了。

在实际开发中,通常不建议这么干,因为全面接管Spring MVC后,自己需要编写的代码会大大增加,更常用的应该是扩展Spring MVC。

5.如何修改Spring Boot的默认配置

模式:

  1. Spring Boot在自动配置的时候,先查看容器中有没有用户自己定义配置的组件(@Bean、@Component),如果有,就使用用户自己配置的,如果没有,就自动配置。有些组件支持多值,比如viewResolver,可以将用户配置和默认配置组合起来同时生效。
  2. Spring Boot中有许多的xxxConfigurer帮助我们进行扩展配置。
  3. Spring Boot中有许多的xxxCustomizer帮助我们进行定制配置。

6.RESTful CRUD

将dao和entity导入,将静态资源导入项目。

1.默认访问首页

启动项目,访问localhost:8080,发现访问不到内容,因为MyMvcConfig类里配置了@EnableWebMvc,去掉之后,再次访问,发现访问到了static目录下的index.html,这是默认的首页。怎么才能访问到templates下的index.html呢?需要走Controller来访问。

前面讲过,Spring Boot访问静态资源的时候,会去这些地方查找资源:classpath:/META‐INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/。它们的优先级是怎么样的呢?

优先级顺序/META-INF/resources>resources>static>public。

这里介绍两种方法:

1.在Controller中加入映射,返回index,从而访问到templates里面的页面。

@RequestMapping({"/", "/index.html"})
public String index() {
    return "index";
}

2.在MyMvcConfig中添加视图映射,可以在MyMvcConfig里原有的addViewControllers()里继续添加,也可以写另外一个方法,返回WebMvcConfigurer,带上@Bean将WebMvcConfigurer注入到容器中,在这个WebMvcConfigurer里做视图映射。

// 所有的WebMvcConfigurer组件都会一起起作用
// 将WebMvcConfigurer注入容器
@Bean
public WebMvcConfigurer webMvcConfigurer() {
    WebMvcConfigurer webMvcConfigurer = new WebMvcConfigurer() {
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/").setViewName("index");
            registry.addViewController("/index.html").setViewName("index");
        }
    };
    return webMvcConfigurer;
}

 找到下面的标签,进行修改即可,最前面的/不要忘了,否则是拿不到资源的。

<link th:href="@{/asserts/css/bootstrap.min.css}" rel="stylesheet">
<link th:href="@{/asserts/css/signin.css}" rel="stylesheet">
<img class="mb-4" th:src="@{/asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72">

2.国际化

  1. 编写国际化配置文件
  2. 使用ResourceBundleMessageSource管理国际化资源文件
  3. 在页面取出国际化内容进行显示

步骤:

1.编写国际化配置文件,抽取页面中需要显示的国际化信息

在resources下创建名称为i18n的文件夹,加入login.properties,login_zh_CN.properties后,此时IDEA识别到这个文件夹是国际化文件夹,会切换为国际化模式,右键继续添加,点击“Add Properties Files to Resource Bundle”,在弹窗中点击右侧加号,输入en_US,添加即可。点击Resource Bundle 'login',点击左上角的“+”号,输入属性的键,会发现页面右侧多出了几个方格,这里就是方便进行国家化的地方,可以方便的设置这个键在不同配置文件下的值是什么。

2.Spring Boot已经帮我们自动配置好了管理国际化资源文件的组件。

打开MessageSourceAutoConfiguration类,它通过messageSourceProperties()方法注入了MessageSourceProperties类,查看MessageSourceProperties类,它里面有一个basename属性,再结合messageSourceProperties()方法上的@ConfigurationProperties(prefix = "spring.messages")可知,通过spring.messages.basename来指定国际化文件夹。于是,我们在配置文件中配置上。看弹幕里有说使用/不能用.,我试了下都可以,在MessageSourceAutoConfiguration类里有getResources()方法,有一个replace()方法,将.换成/,所以.和/都是可以的。

spring.messages.basename=i18n.login

3.去页面获取国际化的值

使用Thymeleaf标签取国际化配置文件中的值,注意提前修改properties的编码,否则会中文乱码。

<form class="form-signin" action="dashboard.html">
    <img class="mb-4" th:src="@{/asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72">
    <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}"></h1>
    <label class="sr-only"th:text="#{login.username}"></label>
    <input type="text" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
    <label class="sr-only" th:text="#{login.password}"></label>
    <input type="password" class="form-control" 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}"></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>

原理:

找到WebMvcAutoConfiguration类中的localeResolver()方法,如果能从配置文件中读取到spring.mvc.locale配置的值,就按照这个值进行国际化,如果读取不到就按照Http请求头中的信息确定国际化。

默认情况下,是没有配置spring.mvc.locale,也就是根据Http请求头中的Accept-Language属性确定的。

4.现在需要实现,点击页面按钮来实现国际化的切换

既然知道了原理,我们可以在点击按钮的时候,发送请求,根据请求头带的参数,修改请求头中的信息,将Locale替换掉即可。

前端页面需要做修改,再编写一个MyLocaleResolver类来处理,并将MyLocaleResolver注入到容器中。

<a class="btn btn-sm" th:href="@{/index.html(language='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(language='en_US')}">English</a>
package com.atguigu.springboot.component;

import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

public class MyLocaleResolver implements LocaleResolver {
    @Override
    public Locale resolveLocale(HttpServletRequest httpServletRequest) {
        String language = httpServletRequest.getParameter("language");
        Locale locale = Locale.getDefault();
        if (!StringUtils.isEmpty(language)) {
            // 因为参数值是en_US或zh_CN的,这里使用带有两个参数的构造器构造Locale
            String[] split = language.split("_");
            locale = new Locale(split[0], split[1]);
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
    }
}

 在MyMvcConfig中注入自己编写的MyLocaleResolver,需要注意的是,这里的方法名必须是localeResolver(),否则无效。

@Bean
public LocaleResolver localeResolver() {
    return new MyLocaleResolver();
}

3.登陆

给form表单添加action和method,给username和password加上name属性,编写对应的controller。

<form class="form-signin" th:action="@{/user/login}" method="post">
    <input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
    <input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required="">
</from>
package com.atguigu.springboot.controller;

import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Map;

@Controller
public class LoginController {
    // @RequestMapping(value = "/user/login", method = RequestMethod.POST)
    // @PostMapping和@RequestMapping(value = "/user/login", method = RequestMethod.POST)是一样的
    // 除了@PostMapping还有@GetMapping、@PutMapping、@DeleteMapping
    @PostMapping(value = "/user/login")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password,
                        Map<String, Object> map) {
        if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
            return "dashboard";
        } else {
            map.put("msg", "用户名密码错误");
            return "login";
        }
    }
}

当登陆失败后,返回登陆页面,将错误信息展示出来,在登陆上面加入p标签,并使用Thymeleaf的th:if标签进行判断。

<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>

登陆成功后,执行的是转发,因此登陆成功页面的url还是以localhost:8080/user/login,此时再刷新就会重新提交表单,为了避免重复提交表单,登陆成功后,需要做一个重定向跳转。在视图解析器里添加一个映射,修改登陆成功后的代码。

// 视图解析器添加一个映射
registry.addViewController("main.html").setViewName("dashboard");
// 登陆成功后,修改返回值,因为配置了main.html的映射,所以会跳到dashboard.html页面,而url继续保持main.html不变
return "redirect:/main.html";

4.拦截器进行登录检查

现在直接访问localhost:8080/main.html可以绕过登陆,这是不行的,需要编写拦截器进行控制,并将拦截器配置到MyMvcConfig中,才能生效。

给login方法添加HttpSession参数,登陆成功后,写session用于后续拦截器判断权限。

public String login(@RequestParam("username") String username,
                    @RequestParam("password") String password,
                    Map<String, Object> map,
                    HttpSession httpSession) {
    if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
        httpSession.setAttribute("loginUser", username);
        return "redirect:/main.html";
    } else {
        map.put("msg", "用户名密码错误");
        return "login";
    }
}
package com.atguigu.springboot.component;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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;
        }
    }
}

在MyMvcConfig类里加入addInterceptor()方法,放在new WebMvcConfigurer(){}里面,需要把静态资源放行。

@Override
public void addInterceptors(InterceptorRegistry registry) {
    // 这个地方,老师在讲的时候说Spring Boot已经帮我们处理好了静态资源的映射,老师使用的是Spring Boot 1.x版本,可能这是1.x版本里的特性
    // 在Spring Boot 2.x版本里,还是需要把静态资源加到放行里面的,否则浏览器拿不到静态资源
    registry.addInterceptor(new LoginHandlerInterceptor())
            .addPathPatterns("/**")// 拦截任意路径下的任意请求
            .excludePathPatterns("/", "/index.html", "/user/login", "/webjars/**", "/asserts/**");// 放行的请求
}

5.CRUD-员工列表

1.RESTful CRUD:CRUD满足REST风格

URI:/资源名称/资源标识,使用HTTP请求方式区分对资源的CRUD操作。

功能普通CRUD(uri区分操作)RESTful CRUD
查询getEmpemp:GET请求
添加addEmpemp:POST请求
修改updateEmpemp:PUT请求
删除deleteEmpemp:DELETE请求

2.员工CRUD请求架构

功能请求URI请求方式
查询所有员工emp/listGET
查询某个员工emp/{id}GET
来到添加页面empGET
添加员工empPOST
来到修改页面emp/{id}GET
修改员工empPUT
删除员工emp/{id}DELETE

3.员工列表

修改dashborad.html页面中的a标签请求地址。

<a class="nav-link" th:href="@{/emp/list}">

编写EmployeeController类来处理员工相关的请求。

package com.atguigu.springboot.controller;

import com.atguigu.springboot.dao.EmployeeDao;
import com.atguigu.springboot.entities.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.Collection;
import java.util.Map;

@Controller
@RequestMapping("/emp")
public class EmpolyeeController {
    @Autowired
    EmployeeDao employeeDao;

    @GetMapping("/list")
    public String list(Map<String, Object> map) {
        Collection<Employee> employees = employeeDao.getAll();
        map.put("emps", employees);
        return "list";
    }
}

4.Thymeleaf公共片段抽取

dashboard.html和list.html中有公共的部分,可以将其抽取出来,在其他页面进行引用。

引入公共片段的方式有两个:

  • ~{templatename::selector}(模板名::选择器)
  • ~{templatename::fragmentname}(模板名::片段名)

在templates下创建commons文件夹,添加bar.html文件,给topbar和sidebar添加属性“th:fragment="片段名称”,用于标识这个片段,将topbar和sidebar放到bar.html中。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--topbar-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">
    ……
</nav>
<!--leftbar-->
<nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar">
    ……
</nav>
</body>
</html>

在dashboard.html和list.html中使用如下代码引入代码片段。

<!--引入抽取的topbar-->
<!--模板名:会使用Thymeleaf的前后缀配置规则进行解析-->
<!--topbar-->
<div th:replace="~{commons/bar::topbar}"></div>
<!--sidebar-->
<div th:replace="~{commons/bar::sidebar}"></div>

Thymeleaf支持3种引入公共片段的th属性:th:insert、th:replace、th:include,具体看示例。

<footer th:fragment="copy">
&copy; 2011 The Good Thymes Virtual Grocery
</footer>
<!--引入方式-->
<div th:insert="footer :: copy"></div>
<div th:replace="footer :: copy"></div>
<div th:include="footer :: copy"></div>
<!--效果-->
<div>
    <footer>
    &copy; 2011 The Good Thymes Virtual Grocery
    </footer>
</div>
<footer>
&copy; 2011 The Good Thymes Virtual Grocery
</footer>
<div>
&copy; 2011 The Good Thymes Virtual Grocery
</div>

5.链接高亮,列表完成

高亮是通过class来加上的,所以可以使用Thymeleaf模板里的判断,根据当前请求的uri来设置class的值。Thymeleaf的代码包含支持传递参数,在做代码包含的时候,传一个参数过去,在bar.html页面,根据传过来的这个值,判断哪个选项高亮即可。

<!--dashboard.html-->
<div th:replace="~{commons/bar::sidebar(activeUri='main.html')}"></div>
<!--list.html-->
<div th:replace="~{commons/bar::sidebar(activeUri='/emp/list')}"></div>
<!--bar.html-->
<!--dashboard的a标签-->
<a th:class="${activeUri=='main.html'? 'nav-link active' : 'nav-link'}" th:href="@{/main.html}">
<!--员工管理的a标签-->
<a th:class="${activeUri=='/emp/list'? 'nav-link active' : 'nav-link'}" th:href="@{/emp/list}">

将main部分换成下面的代码,主要修改就是从请求带过来的map中读取数据并展示, 在上面合适位置加上一个添加按钮。

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <h2><a class="btn btn-sm btn-success">员工添加</a></h2>
    <div class="table-responsive">
        <table class="table table-striped table-sm">
            <thead>
            <tr>
                <th>#</th>
                <th>lastName</th>
                <th>email</th>
                <th>gender</th>
                <th>department</th>
                <th>birth</th>
                <th>操作</th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="emp:${emps}">
                <td th:text="${emp.id}"></td>
                <td th:text="${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>
                    <button class="btn btn-sm btn-primary">编辑</button>
                    <button class="btn btn-sm btn-danger">删除</button>
                </td>
            </tr>
            </tbody>
        </table>
    </div>
</main>

6.CRUD-员工添加

给上面的“员工添加”加上href标签:th:href="@{/emp}",在EmployeeController里加上处理请求,让它返回add页面并把部门信息带过来,在templates里加入add.html。add.html从list.html复制过来,将main里面的东西替换掉。

// 在class上有@RequestMapping("/emp")的注解,所以这里是空
@GetMapping("")
public String toAddPage(Map<String, Object> map) {
    Collection<Department> departments = departmentDao.getDepartments();
    map.put("depts", departments);
    // 返回添加页面
    return "add";
}
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <form th:action="@{/emp}" method="post">
        <div class="form-group">
            <label>LastName</label>
            <input type="text" class="form-control" placeholder="zhangsan" name="lastName">
        </div>
        <div class="form-group">
            <label>Email</label>
            <input type="email" class="form-control" placeholder="zhangsan@atguigu.com" name="email">
        </div>
        <div class="form-group">
            <label>Gender</label><br/>
            <div class="form-check form-check-inline">
                <input class="form-check-inline" type="radio" name="gender" value="1">
                <label class="form-check-label">男</label>
            </div>
            <div class="form-check form-check-inline">
                <input class="form-check-inline" 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" name="department.id">
                <option th:each="dept:${depts}" th:text="${dept.departmentName}" th:value="${dept.id}"></option>
            </select>
        </div>
        <div class="form-group">
            <label>Birth</label>
            <input type="text" class="form-control" placeholder="zhangsan" name="birth">
        </div>
        <button type="submit" class="btn btn-primary">添加</button>
    </form>
</main>

编写处理添加请求的Controller,执行添加操作后重定向到/emp/list。

// 在class上有@RequestMapping("/emp")的注解,所以这里是空
@PostMapping("")
// Spring MVC自动将参数和对象进行绑定,要求参数名和JavaBean的属性名一致
public String addEmp(Employee employee) {
    employeeDao.save(employee);
    // 重定向返回员工列表页面
    return "redirect:/emp/list";
}

在测试添加的格式化,可能碰到一个坑,birth属性必须是yyyy/MM/dd的形式,否则就报错了。可以通过spring.mvc.date-format=yyyy-MM-dd在配置文件中来指定格式,此时yyyy/MM/dd就不能用了,只能是yyyy-MM-dd的格式。因此,这里可以考虑使用日期插件来解决。

7.CRUD-员工修改

给list.html页面的修改按钮为a标签,加上跳转地址:th:href="@{/emp/}+${emp.id}",编写controller接收请求,查询员工信息和部门信息,用于回显,并跳转到edit页面,edit页面是从add页面复制过来的,做了下修改。

@GetMapping("{id}")
public String toEditPage(@PathVariable("id") int id,
                         Map<String, Object> map) {
    Employee employee = employeeDao.get(id);
    map.put("emp", employee);
    Collection<Department> departments = departmentDao.getDepartments();
    map.put("depts", departments);
    // 回到修改页面
    return "edit";
}
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <form th:action="@{/emp/}+${emp.id}" method="post">
        <!--发送PUT请求执行修改-->
        <!--配置HiddenHttpMethodFilter,不同版本的Spring Boot的是否自动配置可能不一样,需要查看WebMvcAutoConfiguration类中hiddenHttpMethodFilter()方法上的注解-->
        <!--@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter",name = {"enabled"},matchIfMissing = false)-->
        <!--我这个版本matchIfMissing = false,表示默认不开启,如果要使用,需要手动开启-->
        <!--手动开启方式:在配置文件中加上spring.mvc.hiddenmethod.filter.enabled=true-->
        <!--页面创建post表单-->
        <!--添加input隐藏域,name="_method" value="put"-->
        <input type="hidden" name="_method" value="put">
        <div class="form-group">
            <label>LastName</label>
            <input type="text" class="form-control" name="lastName" th:value="${emp.lastName}">
        </div>
        <div class="form-group">
            <label>Email</label>
            <input type="email" class="form-control" name="email" th:value="${emp.email}">
        </div>
        <div class="form-group">
            <label>Gender</label><br/>
            <div class="form-check form-check-inline">
                <input class="form-check-inline" type="radio" name="gender" value="1" th:checked="${emp.gender==1}">
                <label class="form-check-label">男</label>
            </div>
            <div class="form-check form-check-inline">
                <input class="form-check-inline" type="radio" name="gender" value="0" th:checked="${emp.gender==0}">
                <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:${depts}" th:text="${dept.departmentName}" th:value="${dept.id}" th:selected="${dept.id==emp.department.id}"></option>
            </select>
        </div>
        <div class="form-group">
            <label>Birth</label>
            <input type="text" class="form-control" name="birth" th:value="${#dates.format(emp.birth,'yyyy-MM-dd HH:mm')}">
        </div>
        <button type="submit" class="btn btn-primary">更新</button>
    </form>
</main>

在修改信息的时候,需要发送PUT请求,但是form是没有PUT请求的,还是需要发送POST表单,另外加一个input项,具体看上面的html页面,这是通过HiddenHttpMethodFilter来处理的。

找到HiddenHttpMethodFilter类中的doFilterInternal()方法,获取“_method”的值,判断“_method”的值是否在ALLOWED_METHODS里面,如果在,就以“_method”的值的方式重新发送请求。

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    HttpServletRequest requestToUse = request;
    if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
        String paramValue = request.getParameter(this.methodParam);
        if (StringUtils.hasLength(paramValue)) {
            String method = paramValue.toUpperCase(Locale.ENGLISH);
            if (ALLOWED_METHODS.contains(method)) {
                requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
            }
        }
    }
    filterChain.doFilter((ServletRequest)requestToUse, response);
}

编写修改处理的Controller。

@PutMapping("{id}")
public String editEmp(Employee employee) {
    // PathVariable中的{id}会自动封装到employee,给employee的id进行赋值
    employeeDao.save(employee);
    return "redirect:/emp/list";
}

8.CRUD-员工删除

删除需要发送DELETE请求,所以需要用一个表单包裹delete按钮,以form的post形式把请求发出去了,同样,需要查看当前Spring Boot是否自动注入了HiddenHttpMethodFilter,如果没有,需要配置上spring.mvc.hiddenmethod.filter.enabled=true。

<form th:action="@{/emp/}+${emp.id}" method="post">
    <!--用于发送delete请求的input隐藏域-->
    <input type="hidden" name="_method" value="delete">
    <button type="submit" class="btn btn-sm btn-danger">删除</button>
</form>

编写删除处理的Controller。

@DeleteMapping("{id}")
public String deleteEmp(@PathVariable("id") int id) {
    employeeDao.delete(id);
    return "redirect:/emp/list";
}

此时会发现页面的样式变了,因为我们使用了from的缘故,把页面的元素挤下来了。可以考虑把from表单扔到外面,点击按钮,通过js的方式把表单发送出去。

7.错误处理机制

1.Spring Boot默认的错误处理机制

  1. 当请求客户端是浏览器时候,返回一个默认的错误页面。
  2. 当请求客户端是其他客户端时候,返回json数据。

原理:参考ErrorMVCAutoConfiguration类:错误处理的自动配置。

1.DefaultErrorAttributes:

DefaultErrorAttribute类的getErrorAttributes()方法,获取错误页面的信息:

  • timestamp:时间戳
  • status:状态码(this.addStatus()方法里)
  • error:错误提示(this.addStatus()方法里)
  • exception:异常对象(this.addErrorDetails()方法里)
  • message:异常消息(this.addErrorDetails()方法里)
  • errors:JSR303数据校验错误(this.addErrorDetails()方法里的this.addErrorMessage()方法获取的BindingResult和JSR303校验有关)
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
    Map<String, Object> errorAttributes = new LinkedHashMap();
    errorAttributes.put("timestamp", new Date());
    this.addStatus(errorAttributes, webRequest);
    this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
    this.addPath(errorAttributes, webRequest);
    return errorAttributes;
}

2.BasicErrorController:处理默认/error请求

通过BasicErrorController的注解可以看到,这个类就是一个Controller,而且RequestMapping对应的值是${server.error.path:${error.path:/error}},这里冒号是用来判断的,当冒号前面的值为空时,整个表达式的值就是后面的值。在BasicErrorController中,观察errorHtml()方法和error()方法,一个用于返回html页面,一个用于返回ResponseEntity,也就是json格式。

// 返回HTML
@RequestMapping(produces = {"text/html"})
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    HttpStatus status = this.getStatus(request);
    Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
    response.setStatus(status.value());
    // 通过resolveErrorView()方法拿到返回的页面和数据
    ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
    return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}

// 返回json数据
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    HttpStatus status = this.getStatus(request);
    if (status == HttpStatus.NO_CONTENT) {
        return new ResponseEntity(status);
    } else {
        Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
        return new ResponseEntity(body, status);
    }
}

浏览器发送请求的时候,请求头里有一个Accept属性,表示希望接收的值的格式是text/html。

再看其他客户端,比如用postman发送一个请求,它的请求头里Accept属性是*/*,所以给它返回了json数据。

3.ErrorPageCustomizer:

在ErrorMvcAutoConfiguration类找到ErrorPageCustomizer内部类,找到registerErrorPages()方法,这里会new一个ErrorPage,通过this.properties.getError()找到ErrorProperties类,这个是ServerProperties类的一个属性,进入ErrorProperties类,可以看到path的值来自配置文件的error.path的值。默认值是/error,此时就用到BasicErrorController类了。

4.DefaultErrorViewResolver:

找到DefaultErrorViewResolver类的resolve()方法,其中viewName来自resolveErrorView()方法的参数,status.series()方法返回Http请求错误码的第一位,SERIES_VIEWS是一个map结构,并在类的静态代码块中做了初始化,放进去了两个对象,分别是{4:4xx}和{5:5xx},通过SERIES_VIEWS的get,于是viewName的值就是4xx或5xx了。

尝试获取模板引擎,如果能获取到,就把用errorViewName构造一个modelAndView扔给模板引擎处理。

如果不能获取到,继续调用resolveReource()方法。在resolveReource()方法中,会去静态资源文件夹中查找错误页面。

public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
    ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
    if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
        modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
    }
    return modelAndView;
}

private ModelAndView resolve(String viewName, Map<String, Object> model) {
    String errorViewName = "error/" + viewName;
    TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
    return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}

private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
    String[] var3 = this.resourceProperties.getStaticLocations();
    int var4 = var3.length;
    for(int var5 = 0; var5 < var4; ++var5) {
        String location = var3[var5];
        try {
            Resource resource = this.applicationContext.getResource(location);
            resource = resource.createRelative(viewName + ".html");
            if (resource.exists()) {
                return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
            }
        } catch (Exception var8) {
        }
    }
    return null;
}

步骤:

程序发生错误(4xx或5xx)的时候,ErrorPageCustomizer类就会生效(定制错误响应规则),发送/error请求,由BasicErrorController来处理。响应页面是由DefaultErrorViewResolver解析得到的。

2.定制错误响应

1.定制错误页面

1.有模板引擎的情况下,会查找templates/error/状态码.html。所以,可以把自定义的4xx和5xx放在对应文件夹下。这里可以使用4xx.html代替所有4开头的错误状态码,使用5xx.html代替所有5开头的错误状态码。并且遵循精确匹配优先,模糊匹配在后的原则。

在模板里可以获取到DefaultErrorAttitudes的值并显示。

<h1>status:[[${status}]]</h1>
<h1>timestamp:[[${timestamp}]]</h1>
<h1>error:[[${error}]]</h1>
<h1>exception:[[${exception}]]</h1>
<h1>message:[[${message}]]</h1>
<h1>errors:[[${errors}]]</h1>

不过,exception的值没有显示出来,原因是:Spring Boot 2.x版本里includeException默认是false的,具体可以查看DefaultErrorAttributes类的初始化方法,给includeException赋值为false,那么,我们可以在配置文件中开启includeException:

# 自定义错误页面,显示exception的内容
server.error.include-exception=true

2.在没有模板引擎的情况下,会从静态资源文件夹下查找错误页面,即查找static/error/状态码.html。因为static文件夹不会经过Thymeleaf模板渲染,所以里面的Thymeleaf语法都无效,如果静态资源使用的是Thymeleaf语法获取的,需要改成普通html语法,另外,它也取不到DefaultErrorAttitudes的值。

3.以上都没有错误页面,就来到Spring Boot的默认错误提示页面,即返回new ModelAndView("error", model);,在ErrorMvcAutoConfiguration类里有一个defaultErrorView()方法,返回默认的error视图。也就是Spring Boot默认的错误提示页面。

2.定制错误json数据

编写一个UserNotExistException类,并写一个controller请求/testException,直接throw这个异常。使用postman访问/testException,会返回一个json数据,现在需要定制这个json数据。

package com.atguigu.springboot.exception;

public class UserNotExistException extends RuntimeException {
    public UserNotExistException() {
        super("用户不存在");
    }
}
// 写一个Controller方法,直接抛出这个异常
@RequestMapping("/testException")
public void testException() {
    // 直接抛出异常
    throw new UserNotExistException();
}

 编写MyExceptionHandler来处理UserNotExistException,请求/testException。

第一种方式,不管是浏览器,还是其他客户端,都返回了json数据,这种方式不太好,我们希望自适应。

第二种方式,将请求进行转发,转发给/error,因为/error后面会做自适应,但是发现跳转到了Spring Boot默认的错误页面,观察此时的状态码是200,因为没有200对应的错误页面,所以返回了默认的错误页面,因此就需要手动设置状态码。

package com.atguigu.springboot.controller;

import com.atguigu.springboot.exception.UserNotExistException;
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;
import java.util.HashMap;
import java.util.Map;

// 要想让这个类成功异常处理器,需要带上@ControllerAdvice注解
@ControllerAdvice
public class MyExceptionHandler {
    // 1.浏览器和postman获取的都是json数据
    @ResponseBody
    @ExceptionHandler(UserNotExistException.class)
    public Map<String, Object> handleException(Exception exception) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", "user.notexist");
        map.put("message", exception.getMessage());
        return map;
    }

    // 2.因为/error请求支持自适应处理,我们将错误请求转发到/error
    @ExceptionHandler(UserNotExistException.class)
    public String handleException(Exception exception,
                                  HttpServletRequest httpServletRequest) {
        Map<String, Object> map = new HashMap<>();
        // 需要自己传入错误状态码,否则就是200,可是200没有对应的error页面,又回到了Spring Boot默认的错误页面了
        // 详情:BasicErrorController中的errorHtml()方法,查看getStatus()方法发现,状态码是request.getAttribute("javax.servlet.error.status_code")
        // 所以我们要自己把状态码改掉
        httpServletRequest.setAttribute("javax.servlet.error.status_code", 400);
        map.put("code", "user.notexist");
        map.put("message", exception.getMessage());
        // 转发到/error完成自适应
        return "forward:/error";
    }
}

3.将定制数据携带出去

上面的第二个方式可以做到自适应,可是不能将数据带出去,观察/error请求处理的两个方法,在返回数据的时候,都调用了一个getErrorAttributes()方法,会获取到一个ErrorAttributes。ErrorAttributes是一个接口,它的实现类是DefaultErrorAttributes,再看ErrorMVCAutoConfiguration类的errorAttributes()方法,它上面有一个注解,@ConditionalOnMissingBean(value = {ErrorAttributes.class}, search = SearchStrategy.CURRENT),也就是ErrorAttributes在容器中不存在的时候,创建一个DefaultErrorAttributes放到容器中,这个DefaultErrorAttributes用于放置attribute。于是,我们可以自己写一个ErrorAttributes,此时DefaultErrorAttributes就不会被注入容器了,在后面执行getErrorAttributes()的时候,我们可以自己控制里面的attribute。

package com.atguigu.springboot.component;

import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;

import java.util.Map;

@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        // 获取errorAttributes
        Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
        // 向errorAttributes中放入自定义信息
        errorAttributes.put("myExceptionInfo", "自定义的用户不存在异常");
        return errorAttributes;
    }
}

再次发送一个/testException请求,发现返回结果中带上了自定义添加的myExceptionInfo。

{"timestamp":"2020-04-12T10:13:58.823+0000","status":400,"error":"Bad Request","message":"用户不存在","path":"/testException","myExceptionInfo":"自定义的用户不存在异常"}

 上面这种方式,需要在MyErrorAttributes添加自定义信息,将异常信息都在MyErrorAttributes里拼装显然不合适,应该交给各自的ExceptionHandler,ExceptionHandler组装好之后,通过request传递出去,MyErrorAttributes根据需要,就从request里取出来一并放到attributes里。

// MyExceptionHandler中的第二种方法修改,将自定义json通过request传递出去
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception exception,
                              HttpServletRequest httpServletRequest) {
    Map<String, Object> map = new HashMap<>();
    // 需要自己传入错误状态码,否则就是200,可是200没有对应的error页面,又回到了Spring Boot默认的错误页面了
    // 详情:BasicErrorController中的errorHtml()方法,查看getStatus()方法发现,状态码是request.getAttribute("javax.servlet.error.status_code")
    // 所以我们要自己把状态码改掉
    httpServletRequest.setAttribute("javax.servlet.error.status_code", 400);
    map.put("code", "user.notexist");
    map.put("message", exception.getMessage());
    // 将map放到request中,在MyErrorAttributes中调用
    httpServletRequest.setAttribute("ext", map);
    // 转发到/error完成自适应
    return "forward:/error";
}
// MyErrorAttributes中getErrorAttributes()方法修改,添加了从request域获取自定义信息
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
    Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
    errorAttributes.put("myExceptionInfo", "自定义的用户不存在异常");
    // 获取MyExceptionHandler中传过来的异常信息
    Map<String, Object> map = (Map<String, Object>) webRequest.getAttribute("ext", RequestAttributes.SCOPE_REQUEST);
    errorAttributes.put("ext", map);
    return errorAttributes;
}

 再次发送一个/testException请求,发现返回结果中带上了MyExceptionHandler中自定义添加的信息。

{"timestamp":"2020-04-12T10:23:33.345+0000","status":400,"error":"Bad Request","message":"用户不存在","path":"/testException","myExceptionInfo":"自定义的用户不存在异常","ext":{"code":"user.notexist","message":"用户不存在"}}

8.配置嵌入式Servlet容器

Spring Boot默认情况下使用内置Tomcat作为嵌入式Servlet容器。

1.定制和修改Servlet容器的配置

1.修改Server有关配置(ServerProperties)

通用Servlet容器设置
server.xxx=xxx
Tomcat的设置
server.tomcat.xxx=xxx

2.编写一个WebServerFactoryCustomizer对Servlet进行定制

package com.atguigu.springboot.config;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyServerConfig {
    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> webServerFactoryCustomizer() {
        return new WebServerFactoryCustomizer<TomcatServletWebServerFactory>() {
            @Override
            public void customize(TomcatServletWebServerFactory factory) {
                factory.setPort(8081);
            }
        };
    }
}

2.注册Servlet三大组件(Servlet、Filter、Listener)

Spring Boot默认以jar包形式启动嵌入式Servlet容器,来启动Spring Boot应用,没有web.xml配置文件。

创建MyServlet,MyFilter,MyListener类。

package com.atguigu.springboot.servlet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 输出一句话
        resp.getWriter().write("Hello MyServlet");
    }
}
package com.atguigu.springboot.filter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpFilter;
import java.io.IOException;

public class MyFilter extends HttpFilter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("MyFilter process");
        chain.doFilter(request, response);
    }
}
package com.atguigu.springboot.listener;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class MyListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("初始化");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("销毁");
    }
}

Servlet使用ServletRegistrationBean注入,Filter使用FilterRegistrationBean注入,Listener使用ServletListenerRegistrationBean注入。

@Bean
public ServletRegistrationBean servletRegistrationBean() {
    return new ServletRegistrationBean(new MyServlet(), "/myServlet");
}

@Bean
public FilterRegistrationBean filterRegistrationBean() {
    FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
    filterRegistrationBean.setFilter(new MyFilter());
    filterRegistrationBean.setUrlPatterns(Arrays.asList("/hello", "/myServlet"));
    return filterRegistrationBean;
}

@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean() {
    return new ServletListenerRegistrationBean<MyListener>(new MyListener());
}

Spring Boot帮我们注册了前端控制器DispatcherServlet,在类DispatcherServletAutoConfiguration类中。

@Bean(name = {"dispatcherServletRegistration"})
@ConditionalOnBean(value = {DispatcherServlet.class}, name = {"dispatcherServlet"})
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
    DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());
    // 默认拦截为:/:所有请求,包括静态资源,不拦截jsp请求
    // /*:拦截所有请求,也拦截jsp请求
    // 可以通过spring.mvc.servlet.path来控制前端控制器拦截的请求路径
    registration.setName("dispatcherServlet");
    registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
    multipartConfig.ifAvailable(registration::setMultipartConfig);
    return registration;
}

3.替换为其他嵌入式Servlet容器

打开pom.xml,右键选择Diagrams-Show Dependencies,找到spring-boot-starter-tomcat,右键选择Exclude,在pom中添加上spring-boot-starter-jetty的依赖。启动项目,此时项目就运行在Jetty中了。同理,Undertow也是类似的方法。

4.嵌入式Servlet容器自动配置原理

视频中讲解的是Spring Boot 1.x版本,现在用的大多数是Spring Boot 2.x版本,所以,这块内容没有办法按照视频上的记录了,因为在Spring Boot 2.x中,有些代码重构了,导致有的类找不到了,于是找了几篇博客看看,学习一下。

查看ServletWebServerFactoryAutoConfiguration类,通过注解,我们可以知道,它在Web环境下运行,当注解条件满足时候,会注入4个类:BeanPostProcessorsRegistrar、EmbeddedTomcat、EmbeddedJetty、EmbeddedUndertow。开启了配置属性的注解,并把ServerProperties作为组件导入,ServerProperties对应的就是配置文件中server开头的一些配置,在里面就包含server.tomcat开头的配置,其中BeanPostProcessorsRegistrar是ServletWebServerFactoryAutoConfiguration的静态内部类,用于注册WebServerFactoryCustomizerBeanPostProcessor和ErrorPageRegistrarBeanPostProcessor。

@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(-2147483648)
@ConditionalOnClass({ServletRequest.class})
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties({ServerProperties.class})
@Import({ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class, EmbeddedTomcat.class, EmbeddedJetty.class, EmbeddedUndertow.class})
public class ServletWebServerFactoryAutoConfiguration {
}

另外3个类属于ServletWebServerFactoryConfiguration的嵌套配置类,根据注解,只有当它们的条件满足时,才会将这个类注入进来作为Web Server,Spring Boot默认使用的Tomcat作为Web Server。

以EmbeddedTomcat为例,查看它上面的注解,@ConditionalOnMissingBean(value = {ServletWebServerFactory.class}, search = SearchStrategy.CURRENT),当ServletWebServerFactory不存在的时候,EmbeddedTomcat生效,ServletWebServerFactory有一个方法getWebServer(),当EmbeddedTomcat生效后,会调用getWebServer()方法,也就是TomcatServletWebServerFactory类中的getWebServer()方法。

protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
    return new TomcatWebServer(tomcat, this.getPort() >= 0);
}

public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
    this.monitor = new Object();
    this.serviceConnectors = new HashMap();
    Assert.notNull(tomcat, "Tomcat Server must not be null");
    this.tomcat = tomcat;
    this.autoStart = autoStart;
    this.initialize();
}

private void initialize() throws WebServerException {
    logger.info("Tomcat initialized with port(s): " + this.getPortsDescription(false));
    synchronized(this.monitor) {
        try {
            this.addInstanceIdToEngineName();
            Context context = this.findContext();
            context.addLifecycleListener((event) -> {
                if (context.equals(event.getSource()) && "start".equals(event.getType())) {
                    this.removeServiceConnectors();
                }
            });
            this.tomcat.start();
            this.rethrowDeferredStartupExceptions();
            try {
                ContextBindings.bindClassLoader(context, context.getNamingToken(), this.getClass().getClassLoader());
            } catch (NamingException var5) {
            }
            this.startDaemonAwaitThread();
        } catch (Exception var6) {
            this.stopSilently();
            this.destroySilently();
            throw new WebServerException("Unable to start embedded Tomcat", var6);
        }
    }
}

在这个方法里,首先会创建一个Tomcat对象,设置连接器等配置,最终调用getTomcatWebServer()方法,当Tomcat获取端口号的值是正数时,创建TomcatWebServer,在TomcatWebServer里,执行initialize()方法,在此方法中调用tomcat的start()方法。

5.嵌入式Servlet容器启动原理

找到ServletWebServerFactoryConfiguration类的tomcatServletWebServerFactory()方法,这个方法用来返回一个嵌入式Servlet容器工厂,在方法里面打上断点,找到TomcatServletWebServerFactory类的getWebServer()方法,在方法里面打上断点。Debug模式运行Spring Boot项目。

1.观察Debug视窗的方法调用栈,从下往上看。

最下面的方法是SpringApplication.run(SpringBoot04WebRestfulcrudApplication.class, args);依次向上代表内层的方法调用,此时方法栈的顶端是我们打断点的tomcatServletWebServerFactory()方法。于是从下往上看就是代码的运行流程。

2.Spring Boot创建IOC容器对象,初始化容器,创建容器中的组件。

传统Web应用使用org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext作为容器,响应式Web应用使用org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext作为容器,其他情况使用org.springframework.context.annotation.AnnotationConfigApplicationContext作为容器。

public ConfigurableApplicationContext run(String... args) {
    ……
    ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
    this.configureIgnoreBeanInfo(environment);
    Banner printedBanner = this.printBanner(environment);
    context = this.createApplicationContext();
    exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
    this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
    this.refreshContext(context);
    this.afterRefresh(context, applicationArguments);
    ……
}

protected ConfigurableApplicationContext createApplicationContext() {
    Class<?> contextClass = this.applicationContextClass;
    if (contextClass == null) {
        try {
            switch(this.webApplicationType) {
            case SERVLET:
                contextClass = Class.forName("org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext");
                break;
            case REACTIVE:
                contextClass = Class.forName("org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext");
                break;
            default:
                contextClass = Class.forName("org.springframework.context.annotation.AnnotationConfigApplicationContext");
            }
        } catch (ClassNotFoundException var3) {
            throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3);
        }
    }
    return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass);
}

3.刷新IOC容器。

调用onRefresh();方法。

4.web的IOC容器,重写了onRefresh()方法。

ServletWebServerApplicationContext类的onRefresh()方法。

protected void onRefresh() {
    super.onRefresh();
    try {
        this.createWebServer();
    } catch (Throwable var2) {
        throw new ApplicationContextException("Unable to start web server", var2);
    }
}

5.web容器创建嵌入式Servlet容器。

ServletWebServerApplicationContext类的createWebServer()方法。

private void createWebServer() {
    WebServer webServer = this.webServer;
    ServletContext servletContext = this.getServletContext();
    if (webServer == null && servletContext == null) {
        ServletWebServerFactory factory = this.getWebServerFactory();
        this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
    } else if (servletContext != null) {
        try {
            this.getSelfInitializer().onStartup(servletContext);
        } catch (ServletException var4) {
            throw new ApplicationContextException("Cannot initialize servlet context", var4);
        }
    }
    this.initPropertySources();
}

6.获取嵌入式的Servlet容器工厂。

ServletWebServerApplicationContext类的getWebServerFactory()方法。

protected ServletWebServerFactory getWebServerFactory() {
    String[] beanNames = this.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
    if (beanNames.length == 0) {
        throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean.");
    } else if (beanNames.length > 1) {
        throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
    } else {
        return (ServletWebServerFactory)this.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
    }
}

7.使用容器工厂获取嵌入式Servlet容器。

this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});

8.嵌入式Servlet容器创建对象并启动Servlet容器。

public WebServer getWebServer(ServletContextInitializer... initializers) {
    if (this.disableMBeanRegistry) {
        Registry.disableRegistry();
    }
    Tomcat tomcat = new Tomcat();
    File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
    tomcat.setBaseDir(baseDir.getAbsolutePath());
    Connector connector = new Connector(this.protocol);
    connector.setThrowOnFailure(true);
    tomcat.getService().addConnector(connector);
    this.customizeConnector(connector);
    tomcat.setConnector(connector);
    tomcat.getHost().setAutoDeploy(false);
    this.configureEngine(tomcat.getEngine());
    Iterator var5 = this.additionalTomcatConnectors.iterator();
    while(var5.hasNext()) {
        Connector additionalConnector = (Connector)var5.next();
        tomcat.getService().addConnector(additionalConnector);
    }
    this.prepareContext(tomcat.getHost(), initializers);
    return this.getTomcatWebServer(tomcat);
}

9.使用外置的Servlet容器

嵌入式Servlet容器:应用打包成jar包。优点:简单、便捷。缺点:默认不支持JSP页面,优化定制比较复杂。

外置Servlet容器:支持JSP,应用打包成war包。

步骤:

1.创建一个war项目。

2.将pom.xml文件里的spring‐boot‐starter‐tomcat指定为provided。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>

表示在编译和测试时使用。

3.必须编写一个SpringBootServletInitializer的子类,并调用configure方法。

package com.atguigu.springboot04;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

public class ServletInitializer extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(SpringBoot04WebJspApplication.class);
    }
}

4.添加外置Tomcat并配置,加入web.xml,启动Tomcat服务。

点开Project Structure,左侧找到Modules,点击Web,找到“Web Resource Directory”,里面已经给出了一个提示的目录,双击这个目录,确定即可创建。再找到上面的Deployment Descriptors右边的加号,点击添加web.xml,注意这里需要修改web.xml的位置。

如果要从Controller返回WEB-INF里的页面,需要在配置文件里写上如下内容: 

spring.mvc.view.prefix=/WEB-INF/
spring.mvc.view.suffix=.jsp

jar包:执行Spring Boot主类main()的时候,启动ioc容器,创建嵌入式Servlet容器。

war包:启动服务器,服务器再启动Spring Boot应用,启动ioc容器,关键点在SpringBootServletInitializer类。

规则:

1.服务器启动,会创建当前web应用里的ServletContainerInitializer实例。

2.ServletContainerInitializer实现类放在jar包的META-INF/service文件夹下,有一个名为
javax.servlet.ServletContainerInitializer的文件,内容就是ServletContainerInitializer的实现类的全类名。

3.还可以使用@HandlesType注解,在应用启动的时候,加载我们感兴趣的类。

原理:

1.启动Tomcat。

2.org\springframework\spring-web\5.2.5.RELEASE\spring-web-5.2.5.RELEASE.jar!\META-INF\services\javax.servlet.ServletContainerInitializer

在Spring的Web模块里,有这个文件:org.springframework.web.SpringServletContainerInitializer。

3.SpringServletContainerInitializer将带有@HandlesTypes(WebApplicationInitializer.class)注解的这个类型的类都传入到onStartup方法的Set中,为这些WebApplicationInitializer创建实例。

4.对每一个WebApplicationInitializer,调用自身的onStartup()方法。

5.SpringBootServletInitializer是WebApplicationInitializer的实现类,因此也会被创建并执行onStartup()方法。

6.SpringBootServletInitializer实例执行onstartup()的时候,会创建容器,即调用createRootApplicationContext()方法。

protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
	// 创建Spring应用构建器
    SpringApplicationBuilder builder = this.createSpringApplicationBuilder();
    builder.main(this.getClass());
    ApplicationContext parent = this.getExistingRootWebApplicationContext(servletContext);
    if (parent != null) {
        this.logger.info("Root context already created (using as parent).");
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, (Object)null);
        builder.initializers(new ApplicationContextInitializer[]{new ParentContextApplicationContextInitializer(parent)});
    }
    builder.initializers(new ApplicationContextInitializer[]{new ServletContextApplicationContextInitializer(servletContext)});
    builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
    // 调用configure()方法,这里的configure()方法就是ServletInitializer类中重写的configure方法,传入了我们自己的SpringBoot应用
    builder = this.configure(builder);
    builder.listeners(new ApplicationListener[]{new SpringBootServletInitializer.WebEnvironmentPropertySourceInitializer(servletContext)});
    // 构建起创建Spring应用
    SpringApplication application = builder.build();
    if (application.getAllSources().isEmpty() && MergedAnnotations.from(this.getClass(), SearchStrategy.TYPE_HIERARCHY).isPresent(Configuration.class)) {
        application.addPrimarySources(Collections.singleton(this.getClass()));
    }
    Assert.state(!application.getAllSources().isEmpty(), "No SpringApplication sources have been defined. Either override the configure method or add an @Configuration annotation");
    if (this.registerErrorPageFilter) {
        application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
    }
    // 启动这个Spring应用
    return this.run(application);
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值