Spring-Boot-Web

Spring Boot Web开发


简介

使用SpringBoot:

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

下面示例的项目可以在我的Github里查看细节

SpringBoot对静态资源的映射规则

webjars

所有 /webjars/** ,都去 classpath:/META-INF/resources/webjars/ 找资源;

在下面的网页寻找webjars的依赖http://www.webjars.org/

然后就可以在以下目录找到

静态资源文件夹

Spring Boot默认使用以下的文件夹作为静态资源文件夹

1
2
3
4
5
"classpath:/META-INF/resources/", 
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
"/":当前项目的根路径

你也可以使用spring.resources.static-location来改变静态资源文件夹路径

1
spring.resources.static-location=classpath:/demo

欢迎页和图标

寻找静态资源文件夹里面的index.htmlfavicon.ico

模板引擎

市面上的模板引擎有JSP、Velocity、Freemarker、Thymeleaf,这些模板引擎的思想都是类似的

Spring Boot推荐使用Thymeleaf,因为它语法更简单,功能更强大。

引入Thymeleaf

引入dependency

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>

Thymeleaf使用

找到/home/ogic/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.1.4.RELEASE/spring-boot-autoconfigure-2.1.4.RELEASE.jar目录下的org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties.class来阅读使用规范

1
2
3
4
5
6
7
8
9
@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";
......

可以看到默认编码是UTF-8,默认使用的路径是classpath:/templates/,默认文件是.html后缀的文件,即html文件。所以我们只要将html文件放在classpath:/templates/下,Thymeleaf就会自动获取。

导入Thymeleaf名称空间

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

这里主要是为了在写html时IDE会给Thymeleaf的语法提示

demo

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--th:text 设置div里面的文本内容 -->
<div th:text="${demo}">这里显示的值会被th获得demo的值覆盖</div>
</body>
</html>

这样子操作的话可以更好的实现前后端分离,因为前端写的格式里带的文本会被后端覆盖,前端在调用后端之前可以显示样板,调用后端后也不会因为前端预设的一些样板收到影响。

th

下面的语法优先级从高到低排列

  1. 片段包含
    • th:insert
    • th:replace
  2. 遍历
    • th:each
  3. 条件判断
    • th:if
    • th:unless
    • th:switch
    • th:case
  4. 声明变量
    • th:object
    • th:with
  5. 全体属性修改
    • th:attr 替换
    • th:attrprepend 前面添加
    • th:attrappend 后面添加
  6. 指定属性默认值修改
    • th:value
    • th:href
    • th:src
    • ……(任意html属性)
  7. 标签内容修改
    • th:text 转义特殊字符
    • th:utext 不转义特殊字符
  8. 片段声明
    • th:fragment
  9. 片段移除
    • th:remove

表达式

详细演示可以看Thymeleaf文档

  • Simple expressions: 表达式语法

    • Variable Expressions: ${…}

      • 获取对象属性
        • ${person.father.name}
          ${person[‘father’][‘name’]}
        • ${countriesByCode.ES}
          ${personsByName[‘Stephen Zucchini’].age}
        • ${personsArray[0].name}
      • 调用方法
        • ${person.createCompleteName()}
          ${person.createCompleteNameWithSeparator(‘-‘)}
      • 使用内置基本对象
        • ${#locale.country}将内置基本对象放入${}里,能用的对象如下
        • #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.
      • 内置工具对象
        • #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: *{…} 选择变量表达式,在功能上与${}一致,多了拓展功能,在使用对象时可以更加方便,例如下面两种写法是等效的:

      • 1
        2
        3
        4
        5
        <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>
      • 1
        2
        3
        4
        5
        <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>
    • Message Expressions: #{…} 取国际化内容(多语言适配)

    • Link URL Expressions: @{…} 定义URL

      • 1
        2
        3
        4
        5
        6
        7
        8
        9
        <!-- Will produce 'http://localhost:8080/gtvg/order/details?orderId=3' (plus rewriting) -->
        <a href="details.html"
        th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a>

        <!-- Will produce '/gtvg/order/details?orderId=3' (plus rewriting) -->
        <a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>

        <!-- Will produce '/gtvg/order/3/details' (plus rewriting) -->
        <a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>
    • 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: _

Thymeleaf缓存

Spring Boot官方文档建议我们使用模板引擎(包括且不限于Thymeleaf)时禁用对应的缓存,也就是在application.properties加上

1
spring.thymeleaf.cache=false

这样当你只是修改html或css的时候只要Ctrl+F9提交以下就能实时更新,无需重启项目。

Thymeleaf抽取公共片段

当我们洗完某个HTML中的某个片段可以被重用时(例如不用的页面有相同的侧边栏或顶部栏),我们可以使用Thymeleaf来完成这一操作而不是通过复制粘贴。

在需要被抽取的片段上加上th:fragment="[fragmentname]"来抽取,例如:

1
2
3
<div th:fragment="copy">
&copy; 2011 The Good Thymes Virtual Grocery
</div>

然后在指定位置引入

1
<div th:insert="~{footer :: copy}"></div>

注意这里是用~{templatename::fragmentname}来指定引入的片段,除此之外还可以使用~{templatename::selector}来指定选择器,这里的templatename是模块名,往往是你的html文件在template路径下的名字(注意不需要.html后缀)

如果你不使用th:fragment来抽取

1
2
3
<div id="example">
&copy; 2011 The Good Thymes Virtual Grocery
</div>

你在引入时就需要

1
<div th:insert="~{footer :: #example}"></div>

除了th:insert外你还可以使用th:replaceth:include来引入,使用一个例子来观察这三者的区别:

有一个待引入的片段:

1
2
3
<footer th:fragment="copy">
&copy; 2011 The Good Thymes Virtual Grocery
</footer>
  • th:insert

    • 将公共片段整个插入到声明引入的元素中

    • 1
      2
      3
      4
      5
      <div>
      <footer>
      &copy; 2011 The Good Thymes Virtual Grocery
      </footer>
      </div>
  • th:replace

    • 将声明引入的元素替换为公共片段

    • 1
      2
      3
      <footer>
      &copy; 2011 The Good Thymes Virtual Grocery
      </footer>
  • th:include

    • 将被引入的片段的内容包含进这个标签中

    • 1
      2
      3
      <div>
      &copy; 2011 The Good Thymes Virtual Grocery
      </div>

如果要使用参数来引入片段,可以在抽取时就指定变量,然后在后续使用

1
2
3
4
5
6
<footer th:fragment="copy(para1,para2)">
&copy; 2011 The Good Thymes Virtual Grocery
</footer>

<div th:text="${para1}"/>
<div th:text="${para2}"/>

到引入时可以

1
<div th:insert="~{footer :: copy(${val1},${val2})}"></div>

或者

1
2
3
<div th:insert="~{footer :: copy(para1=${val1}, para2=${val2})}"></div>

<div th:insert="~{footer :: copy(para2=${val2}, para1=${val1})}"></div>

如果你没有在抽取时声明变量,你就必须使用第二种

Spring MVC配置

Spring MVC auto-configuration

Spring Boot 会自动配置Spring MVC,所使用的默认配置如下

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans

    • ViewResolver:视图解析器,根据方法的返回值获得视图对象(View),视图对象决定如何渲染(例如转发还是重定位)。
    • ContentNegotiatingViewResolver:组合所有的视图解析器
    • 定制方法:自己给容器中添加一个视图解析器,它自动的将其组合进来;
  • Support for serving static resources, including support for WebJars

    • 静态资源文件夹
  • Static index.html support

    • 静态首页访问
  • Custom Favicon support

    • 网页图标
  • Automatic registration of Converter, GenericConverter, and Formatter beans.

    • 自动注册以下内容
    • Converter:转换器,进行类型转换
    • Formatter:格式化器,转换格式
      • 只有我们在文件中配置来日期格式化的规则才会注册,如需添加格式化转换器,只要添加到容器即可
  • Support for HttpMessageConverters

    • HttpMessageConverter:Spring MVC 用于转换HTTP请求和响应
    • HttpMessageConverters 是从容器中确定的,从容器中获取所有的HttpMessageConverter
    • 要加自己的HttpMessageConverter也是将其添加到容器即可
  • Automatic registration of MessageCodesResolver

    • 定义错误代码生成规则
  • Automatic use of a ConfigurableWebBindingInitializer bean

    • 初始化WebDataBinder(数据绑定器)来请求数据
    • ConfigurableWebBindingInitializer也是从容器中获取的,可以添加自己的组件到容器中来替换默认的。

扩展SpringMVC

编写一个配置类(@Configuration),是WebMvcConfigurerAdapter类型;不能标注@EnableWebMvc

1
2
3
4
5
6
7
8
9
10
11
//使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {

@Override
public void addViewControllers(ViewControllerRegistry registry) {
// super.addViewControllers(registry);
//浏览器发送 /demo 请求来到 demoView
registry.addViewController("/demo").setViewName("demoView");
}
}

原理:

  1. WebMvcAutoConfiguration是SpringMVC的自动配置类
  2. 在做其他自动配置时会导入;@Import(EnableWebMvcConfiguration.class)
  3. 容器中所有的WebMvcConfigurer都会一起起作用;
  4. 我们的配置类也会被调用;

效果:SpringMVC的自动配置和我们的扩展配置都会起作用;

全面接管SpringMVC

如果不想要所有SpringMVC自动配置,只要使用@EnableWebMvc即可,当使用@EnableWebMvc注释时。

1
2
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {

@EnableWebMvc会引入一个DelegatingWebMvcConfiguration配置类

1
2
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {

而这个配置类继承自WebMvcConfigurationSupport,我们再看SpringMVC中的代码

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class,
WebMvcConfigurerAdapter.class })

//容器中没有这个组件的时候,这个自动配置类才生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {

里面有一个@ConditionalOnMissingBean(WebMvcConfigurationSupport.class),当容器中没有WebMvcConfigurationSupport配置类组件时,自动配置才会生效,而WebMvcConfigurationSupport只有SpringMVC最基本的功能。

RestfulCRUD

引入Bootstrap

静态资源引入

将Bootstrap的视图放在templates文件夹中,然后如果要使用templates中的视图(html文件)。可以使用两种方法来添加:

Controller里加

在Controller类中加入

1
2
3
4
@RequestMapping({"/demo","/other"})
public String demo(){
return "demo"
}
  • @RequestMapping({“/demo”,”/other”}):响应浏览器的/demo请求或/other请求

  • 方法名随意

  • return “demo”:html名(视图名)

  • 要注意的是尽量不要使用index.html这个名字,虽然不会找成问题或歧义(就是说不会使用在静态资源文件夹下的index,而是会使用你自己引入的index,你自己引入的index放在模板引擎的资源文件夹templates)

    1
    2
    3
    4
    5
    resources
    static
    index.html
    templates
    index.html

    例如这里,在你配置之前会使用static下的index.html,但是你配置后,模板引擎会去找templates目录下的index.html。也就是配置后templates里的index.html就会被使用。

    但是不管怎么说,看着就觉得怪,建议将demo里的index.html改名

Configuration里加

在Configuration里加入

1
2
3
4
5
6
7
8
@Configuration
public class demoAdapter extends WebMvcConfigurerAdapter{

@Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/demo").setViewName("demo");
    }
}

或者:

1
2
3
4
5
6
7
8
9
10
@Bean //将组件注册在容器
public WebMvcConfigurerAdapter webMvcConfigurerAdapter(){
WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/demo").setViewName("demo");
}
};
return adapter;
}

实现效果等同与上面

  • addViewController(“/demo”):响应浏览器的/demo请求或/other请求
  • setViewName(“demo”);html名(视图名)

但是,当你使用较新的Spring Boot时,你会发现 WebMvcConfigurerAdapter 上被一条横线划掉,代表说这个类已经过时了(老东西你的替身最没用了),查阅网上的说法,建议换成 WebMvcConfigurer或者WebMvcConfigurationSupport,这里要注意的是(网上不仅没讲清楚还直接推荐用后者):如果你使用WebMvcConfigurationSupport的话,自动配置会失效(至于为什么上面说了),然后你就会发现一堆错误,例如No mapping for GET XXX.css,因为无论是你的静态资源还是webjars都不会自动添加映射了,虽然你可以通过在配置类里重写addResourceHandlers()添加静态资源映射来解决这一错误,但是我试了以下,静态资源是映射了,但是这些文件无法传到浏览器上(到底是为什么呢?我也在找原因),如果不清楚自动配置帮我们干了什么(没那个水平),还是通过实现WebMvcConfigurer接口来添加viewController吧。

1
2
3
4
5
6
7
8
9
@Configuration
public class MyConfig implements WebMvcConfigurer {

@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/demo").setViewName("demo");
}

}
1
2
3
4
5
6
7
8
9
10
@Bean
public WebMvcConfigurer webMvcConfigurer() {
WebMvcConfigurer adapter = new WebMvcConfigurer() {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/demo").setViewName("demo");
}
};
return adapter;
}

webjars引入

1
2
3
4
5
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>4.3.1</version>
</dependency>

然后在html中引入

1
<link href="[原来的css路径,不需要去掉,模板引擎加载后会用下面路径的css覆盖]" th:href="@{/webjars/bootstrap/4.3.1/css/bootstrap.css}" rel="stylesheet">

具体路径到/home/ogic/.m2/repository/org/webjars/bootstrap/4.3.1/bootstrap-4.3.1.jar/META-INF/resources/webjars/下去找,注意webjars前面的路径不用输入。

如果使用自己的静态资源,也最好加上th:href="@{/demo/target.css}"它就会去静态资源文件夹里找demo子文件夹,然后找里面的target.css

如果你在静态资源文件夹里添加了webjars文件夹,使用th:href="@{/webjars/target.css,其实你自己的webjars是不会被访问的。因为Thymeleaf规定了所有的/webjars请求都去依赖包里找而非静态资源文件夹。

国际化

像下面一样创建好properties文件

分别设置各种语言的信息

application.properties里添加(因为默认是类路径下的messages文件,这里和默认不一样)

1
spring.messages.basename=messages/login

在html中要修改国际化信息的地方加上th:text = "#{login.tip}"或者[[ #{login.tip} ]]

1
2
3
<p th:text = "#{login.tip}">
[[ #{login.tip} ]]
</p>

两个选一个就够了,之后网页就会根据浏览器的语言信息切换语言,如果要用自己的方法切换语言(例如按超链接)

1
2
<a class="btn btn-sm" th:href="@{/(lan='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/(lan='en_US')}">English</a>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class MyLocaleResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
String lan = request.getParameter("lan");
Locale locale = Locale.getDefault();
if (!StringUtils.isEmpty(lan)){
String[] split = lan.split("_");
locale = new Locale(split[0],split[1]);
}
return locale;
}

@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

}
}

然后在配置类将这个方法添加到容器中(注意使用@Component只是把这个类添加到IOC容器中,这个组件并不会影响应用上下文,是不会生效的)

1
2
3
4
5
@Bean
@Autowired
public LocaleResolver localeResolver(LocaleResolver localeResolver){
return localeResolver;
}

从IOC容器中获得这个组件自动注入到方法的参数中,然后return将其添加到bean,这时候它就会发挥作用了。

post请求

给登陆界面的form表单添加一个action和method来让它发送post请求

1
<form class="form-signin" action="dashboard.html" th:action="@{/user/login}" th:method="post">

写一个Controller来处理请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
public class LoginController {

@PostMapping(value = "/user/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
Map<String, Object> map){
if (!StringUtils.isEmpty(username) && "pass".equals(password)){
return "dashboard";
}
map.put("msg", "username and password incorrect");
return "login";
}
}

在合适的位置放一个错误信息显示区域

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

这里只有msg不为空才显示

拦截器机制

添加一个拦截器组件,让其只有session中的loginUser值不为空时才放行,拦截时将页面重定向到登陆页并给msg添加错误信息,使其在登陆页面显示错误(login.html的配置在上面配了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
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", "please log in first");
request.getRequestDispatcher("/login").forward(request,response);
return false;
}
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 {

}
}

修改LoginController,使其登陆时给session传值,加一个HttpSession并设置属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class LoginController {

@PostMapping(value = "/user/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
Map<String, Object> map, HttpSession session){
if (!StringUtils.isEmpty(username) && "pass".equals(password)){
session.setAttribute("loginUser",username);
return "redirect:/main";
}
map.put("msg", "username and password incorrect");
return "login";
}
}

在配置类中加入拦截器,并将所有页面(除登陆页面外)拦截

1
2
3
4
5
6
7
8
@Autowired
HandlerInterceptor handlerInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(handlerInterceptor).addPathPatterns("/**")
.excludePathPatterns("/","/user/login","/login");
}

Tymeleaf也可以获得session中的loginUser值,然后显示当前登陆的用户名

1
<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#" th:text="${session.loginUser}">Company name</a>

CRUD-员工列表

由于Rest思想要求我们通过URL的形式来访问资源,所以我们要确定各个操作的ID

实验功能 请求URI 请求方式
查询所有员工 emp GET
查询某个员工(来到修改页面) emp/[id] GET
来到添加页面 add-emp GET
添加员工 add-emp POST
来到修改页面(查出员工进行信息回显) emp/[id] GET
修改员工 emp/[id] PUT
删除员工 emp/[id] DELETE

写一个Controller来返回列表页面

1
2
3
4
5
6
7
8
9
10
11
@Controller
public class EmployeeController {
@Autowired
EmployeeDao employeeDao;
@GetMapping("/emps")
public String employeeList(Model model){
Collection<Employee> employees = employeeDao.getAll();
model.addAttribute("emps", employees);
return "emps/list";
}
}

将列表放在model中,key为emps然后在html中使用,顺便添加一些按钮以及相应的页面跳转链接给后续使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<h2>
<h2>
<a class="btn" th:class="'btn btn-success btn-block'"
href="emp/add.html" th:href="@{/add-emp}">
Add Customer
</a>
</h2>
</h2>
<table class="table table-striped table-sm">
<thead>
<tr>
<th>ID</th>
<th>lastName</th>
<th>email</th>
<th>gender</th>
<th>department</th>
<th>bitrh</th>
<th>operation</th>
</tr>
</thead>
<tbody>
<tr th:each="emp:${emps}">
<td th:text="${emp.getId()}"/>
<td th:text="${emp.getLastName()}"/>
<td th:text="${emp.getEmail()}"/>
<td th:text="${emp.getGender()}=='0'?'女':'男'"/>
<td th:text="${emp.getDepartment().getDepartmentName()}"/>
<td th:text=
"${#dates.format(emp.getBirth(),'YYYY/MM/dd HH:mm')}"/>
<td>
<a class="btn" th:class="'btn btn-sm btn-primary'">
edit
</a>
<button class="btn" th:class="'btn btn-sm btn-danger'">
delete
</button>
</td>
</tr>
</tbody>
</table>

这里要注意的是th:class中的引号,如果你只有一个class,那么引号内直接写即可,如果你有多个class,那么要多一个引号,因为如果少了这个引号,Themeleaf会无法解析,因为该模板引擎无法解析btn btn-sm btn-danger这样的东西,而必须是'btn btn-sm btn-danger'这样的字符串。毕竟你写class=btn btn-sm btn-danger而不是class='btn btn-sm btn-danger'也是会报错的。

成功后界面如下:

CRUD-员工添加

新建一个员工添加界面emp/add.xml,然后引入顶部栏和侧边栏后(可以直接使用emp/list.xml内的内容把main里的删掉)添加一个输入的表单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<form th:action="@{/add-emp}" th:method="post">
<div class="form-group">
<label>LastName</label>
<input th:name="lastName" type="text"
class="form-control" placeholder="zhangsan">
</div>
<div class="form-group">
<label>Email</label>
<input th:name="email" 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">male</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">female</label>
</div>
</div>
<div class="form-group">
<label>department</label>
<select class="form-control" th:name="department.id">
<option th:value="${dept.getId()}" th:each="dept:${depts}"
th:text="${dept.getDepartmentName()}"/>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input th:name="birth" type="text"
class="form-control" placeholder="1999/06/04">
</div>
<button th:attr="deleteUri=@{/emp/}+${emp.getId()}"
class="btn" th:class="'btn btn-sm btn-danger btn-delete'">
delete
</button>
</form>

在控制器中响应这个Get请求(获得添加页面):

1
2
3
4
5
6
@GetMapping("/add-emp")
public String toAddEmployeePage(Model model){
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("depts",departments);
return "emp/add";
}

在控制器里响应这个Post请求(点击add按钮提交):

1
2
3
4
5
@PostMapping("/add-emp")
public String addEmp(Employee employee){
employeeDao.save(employee);
return "redirect:/emp";
}

效果:

CRUD-员工修改

这里可以重用员工添加的界面,不过需要做一些修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<form th:action="${emp==null}?@{/add-emp}:@{/emp/}+${emp.getId()}" th:method="post">
<input th:type="hidden" th:name="_method"
th:value="put" th:if="${emp!=null}">
<div class="form-group">
<label>LastName</label>
<input th:name="lastName" type="text"
class="form-control" placeholder="zhangsan"
th:value="${emp!=null}?${emp.getLastName()}">
</div>
<div class="form-group">
<label>Email</label>
<input th:name="email" type="email"
class="form-control" placeholder="zhangsan@atguigu.com"
th:value="${emp!=null}?${emp.getEmail()}">
</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.getGender()==1}">
<label class="form-check-label">male</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.getGender()==1}">
<label class="form-check-label">female</label>
</div>
</div>
<div class="form-group">
<label>department</label>
<select class="form-control" th:name="department.id">
<option th:value="${dept.getId()}" th:each="dept:${depts}"
th:text="${dept.getDepartmentName()}"
th:selected="${emp!=null}?
${dept.getId()==emp.getDepartment().getId()}"/>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input th:name="birth" type="text"
class="form-control" placeholder="1999/06/04"
th:value="${emp!=null}?
${#dates.format(emp.getBirth(),'YYYY/MM/dd HH:mm')}">
</div>
<button type="submit" class="btn btn-primary"
th:text="${emp!=null}?'edit':'add'"/>
</form>

在Controller里处理该请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@GetMapping("/emp/{id}")
public String toEditPage(@PathVariable("id") Integer id, Model model){
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("depts",departments);
Employee employee = employeeDao.get(id);
model.addAttribute("emp",employee);
return "emp/add";
}

@PutMapping("/emp/{id}")
public String updateEmp(@PathVariable("id") Integer id, Employee employee){
employee.setId(id);
employeeDao.save(employee);
return "redirect:/emp";
}

CRUD-员工删除

<main>外添加一个删除表单用于删除

1
2
3
<form th:id="deleteEmpForm" th:method="post">
<input th:type="hidden" th:name="_method" th:value="delete">
</form>

给删除按钮添加行为,发送delelte请求

1
2
3
4
5
6
7
8
<script>
//delete employee
$(".btn-delete").click(function(){
$("#deleteEmpForm").attr("action",$(this).attr("deleteUri"))
.submit();
return false;
});
</script>

Controller里响应该请求

1
2
3
4
5
@DeleteMapping("/emp/{id}")
public String deleteEmp(@PathVariable("id") Integer id){
employeeDao.delete(id);
return "redirect:/emp";
}

错误处理机制

  1. 一但系统出现4xx或者5xx之类的错误;
  2. ErrorPageCustomizer生效(定制错误的响应规则);
  3. 来到/error请求;
  4. BasicErrorController处理;

定制错误响应

定制错误提示页面

  1. 有模板引擎找templates下的error文件夹
  2. 没有模板引起找静态资源文件夹
  3. 都没有就默认来到SpringBoot默认的错误提示页面

定制错误提示json数据

  1. 自定义异常处理&返回定制json数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @ControllerAdvice
    public class MyExceptionHandler {

    @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;
    }
    }
  2. 转发到/error进行自适应响应效果处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @ExceptionHandler(UserNotExistException.class)
    public String handleException(Exception e, HttpServletRequest request){
    Map<String,Object> map = new HashMap<>();
    request.setAttribute("javax.servlet.error.status_code",500);
    map.put("code","user.notexist");
    map.put("message",e.getMessage());
    //转发到/error
    return "forward:/error";
    }
  3. 将我们的定制数据携带出去

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Component
    public class MyErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
    Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
    map.put("company","ogic");
    map.put("myMessage","demo")
    return map;
    }
    }

配置嵌入式Servlet容器

如何定制和修改Servlet容器的相关配置

  1. 配置application.properties

    1
    2
    3
    4
    server.port=8081
    server.context-path=/crud

    server.tomcat.uri-encoding=UTF-8
  2. 编写一个EmbeddedServletContainerCustomizer:嵌入式的Servlet容器的定制器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Bean  //将这个定制器加入到容器中
    public EmbeddedServletContainerCustomizer demo(){
    return new EmbeddedServletContainerCustomizer() {

    //定制嵌入式的Servlet容器相关的规则
    @Override
    public void customize(
    ConfigurableEmbeddedServletContainer container) {
    container.setPort(8083);
    }
    };
    }

注册Servlet三大组件【Servlet、Filter、Listener】

  1. ServletRegistrationBean

    1
    2
    3
    4
    5
    @Bean
    public ServletRegistrationBean myServlet(){
    ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(),"/myServlet");
    return registrationBean;
    }
  2. FilterRegistrationBean

    1
    2
    3
    4
    5
    6
    7
    @Bean
    public FilterRegistrationBean myFilter(){
    FilterRegistrationBean registrationBean = new FilterRegistrationBean();
    registrationBean.setFilter(new MyFilter());
    registrationBean.setUrlPatterns(Arrays.asList("/hello","/myServlet"));
    return registrationBean;
    }
  3. ServletListenerRegistrationBean

    1
    2
    3
    4
    5
    @Bean
    public ServletListenerRegistrationBean myListener(){
    ServletListenerRegistrationBean<MyListener> registrationBean = new ServletListenerRegistrationBean<>(new MyListener());
    return registrationBean;
    }

替换为其他嵌入式Servlet容器

  • Tomcat(默认使用)

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    引入web模块默认就是使用嵌入式的Tomcat作为Servlet容器;
    </dependency>
  • Jetty

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- 引入web模块 -->
    <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>

    <!--引入其他的Servlet容器-->
    <dependency>
    <artifactId>spring-boot-starter-jetty</artifactId>
    <groupId>org.springframework.boot</groupId>
    </dependency>
  • Undertow

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- 引入web模块 -->
    <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>

    <!--引入其他的Servlet容器-->
    <dependency>
    <artifactId>spring-boot-starter-undertow</artifactId>
    <groupId>org.springframework.boot</groupId>
    </dependency>

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

  1. SpringBoot根据导入的依赖情况,给容器中添加相应的EmbeddedServletContainerFactory->TomcatEmbeddedServletContainerFactory

  2. 容器中某个组件要创建对象就会惊动后置处理器:EmbeddedServletContainerCustomizerBeanPostProcessor,只要是嵌入式的Servlet容器工厂,后置处理器就工作;

  3. 后置处理器,从容器中获取所有的EmbeddedServletContainerCustomizer,调用定制器的定制方法

嵌入式Servlet容器启动原理

先启动嵌入式的Servlet容器,再将ioc容器中剩下没有创建出的对象获取出来

  1. SpringBoot应用启动运行run方法

  2. refreshContext(context);

    • SpringBoot刷新IOC容器【创建IOC容器对象,并初始化容器,创建容器中的每一个组件。
    • 如果是web应用创建AnnotationConfigEmbeddedWebApplicationContext
    • 否则:AnnotationConfigApplicationContext
  3. refresh(context)

    • 刷新刚才创建好的ioc容器;
  4. onRefresh()

    • web的ioc容器重写了onRefresh方法
  5. createEmbeddedServletContainer()

    • webioc容器会创建嵌入式的Servlet容器
  6. EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();

    • 获取嵌入式的Servlet容器工厂
    • 从ioc容器中获取EmbeddedServletContainerFactory组件
    • TomcatEmbeddedServletContainerFactory创建对象,后置处理器一看是这个对象,就获取所有的定制器来先定制Servlet容器的相关配置;
  7. this.embeddedServletContainer = containerFactory.getEmbeddedServletContainer(getSelfInitializer());

    • 使用容器工厂获取嵌入式的Servlet容器
  8. 嵌入式的Servlet容器创建对象并启动Servlet容器

使用外置的Servlet容器

  1. 必须创建一个war项目(利用idea创建好目录结构)

  2. 将嵌入式的Tomcat指定为provided

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
    </dependency>
  3. 必须编写一个SpringBootServletInitializer的子类,并调用configure方法

  4. 启动服务器