第四章 Spring Boot Web 开发
1、web 开发简介
https://start.spring.io/
- 创建 SpringBoot 应用,选中需要的模块
- 使用 SpringBoot 自动配置
- 编写业务代码
*AutoConfiguration 自动配置组件 *Properties 封装配置文件的内容
webjars&静态资源映射规则
1、webjars
配置类:WebMvcAutoConfiguration
webjars 以 jar 包的方式引入静态资源
https://www.webjars.org/
资源路径映射
/webjars/** => classpath:/META-INF/resources/webjars/
添加 jquery 依赖
<dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.5.1</version> </dependency>
访问路径
/webjars/jquery/3.5.1/jquery.js
2、静态资源映射规则
静态资源文件夹
classpath:/META-INF/resources/ classpath:/resources/ classpath:/static/ classpath:/public/ / 当前项目根路径
默认静态文件下查找
# 欢迎页面 index.html # 图标路径 favicon.ico
自定义静态资源文件路径,默认资源路径失效
spring.resources.static-locations=classpath:/hello/
引入 thymeleaf
JSP、Velocity、Thymeleaf、Freemarker
模板引擎
Template ${name} + Data {"name": "Tom"} => TemplateEngine => output
Thymeleaf 依赖
<properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- 切换 thymeleaf version --> <!-- thymeleaf3 适配 layout2 --> <springboot-thymeleaf.version>2.1.1.RELEASE</springboot-thymeleaf.version> <thymeleaf-layout-dialect.version>2.0.0</thymeleaf-layout-dialect.version> </properties> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <version>${springboot-thymeleaf.version}</version> </dependency>
thymeleaf 语法
https://www.thymeleaf.org/
默认配置
public class ThymeleafProperties { private String prefix = "classpath:/templates/"; private String suffix = ".html"; }
模板使用示例
package com.example.demo.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import java.util.HashMap; @Controller public class IndexController { @RequestMapping("/hello") public String hello(HashMap<String, Object> map){ map.put("name", "Tom"); // 模板路径 // src/main/resources/templates/about.html return "hello"; } }
模板:
src/main/resources/templates/about.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>Title</title> </head> <body> <h1>Hello</h1> <!-- 设置文本内容 --> <div th:text="${name}"></div> </body> </html>
语法规则
th: 任意html属性,用来替换原生属性的值 th:text 改变文本内容(转义) th:utext 改变文本内容(不转义) th:attr th:href th:src th:each th:for
表达式
${} 变量表达式 获取变量值 获取变量属性 调用方法 内置基本对象: #ctx #session... 内置工具对象: *{} 选择表达式 配合th:object使用 #{} 获取国际化内容 @{} 定义url ~{} 片段表达式 字面量 数学运算 布尔运算 比较运算 条件运算 特殊操作
示例
<!--文本输出--> <div th:text="${name}"></div> <!--循环遍历--> <div th:each="pet: ${pets}"> <div>[[${pet}]]</div> </div> <!--循环遍历--> <div th:each="pet: ${pets}" th:text="pet"></div>
SpringMVC 自动配置原理
SpringBoot 对 SpringMVC 默认配置
自动配置
ViewResolver 视图解析器 根据方法返回值的到视图对象(View) 视图对象决定如何渲染、转发、重定向 Converter 类型转换器 Formatter 格式化器 HttpMessageConverters 转换请求响应 MessageCodesResolver 定义错误代码生成规则 WebDataBinder 数据绑定器
修改 SpringBoot 默认配置
优先使用用户配置@Bean/@Component
如果没有才自动配置
有些组件可以有多个
eg: ViewResolver 将用户配置和默认配置组合起来
扩展与全面接管 SpringMVC
1、扩展配置
package com.example.demo.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @Configuration public class CustomVmcConfig extends WebMvcConfigurerAdapter { @Override public void addViewControllers(ViewControllerRegistry registry) { // super.addViewControllers(registry); // 浏览器的请求 /demo 到视图 /hello registry.addViewController("demo").setViewName("hello"); } }
2、全面接管
增加 @EnableWebMvc
后,自动配置失效
package com.example.demo.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @EnableWebMvc @Configuration public class CustomVmcConfig extends WebMvcConfigurerAdapter { @Override public void addViewControllers(ViewControllerRegistry registry) { // super.addViewControllers(registry); // 浏览器的请求 /demo 到视图 /hello registry.addViewController("demo").setViewName("hello"); } }
引入资源
模板资源: https://getbootstrap.net/
模板语法: https://www.thymeleaf.org/
webjars: https://www.webjars.org/
目录设置
resources/ templates 模板文件 static 静态文件
首页设置
package com.example.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @Configuration public class CustomVmcConfig extends WebMvcConfigurerAdapter { // 设置首页位置,默认访问 public/index.html 没有经过模板引擎处理 @Bean public WebMvcConfigurerAdapter CustomVmcConfig() { WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("login"); registry.addViewController("/index.html").setViewName("login"); } }; return adapter; } }
国际化
默认根据浏览器语言获取对应国际化信息
1、配置语言文件
resources 资源文件夹下
├── i18n │ ├── login.properties │ ├── login_en_US.properties │ └── login_zh_CN.properties
默认配置 login.properties
login.button=登录~ login.title=登录~ login.username=用户名~ login.password=密码~ login.remember=记住我~
英文配置 login_en_US.properties
login.button=Sign In login.title=Login login.username=UserName login.password=Password login.remember=Remenber Me
中文配置 login_zh_CN.properties
login.button=登录 login.title=登录 login.username=用户名 login.password=密码 login.remember=记住我
2、配置 application.yml
spring: messages: basename: i18n.login
3、模板文件中使用
<button th:text="#{login.button}"></button>
根据浏览器请求头设置语言
GET http://localhost:8080/ Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
4、自定义国际化处理器
package com.example.demo.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 request) { String lang = request.getParameter("lang"); Locale locale = Locale.getDefault(); if (!StringUtils.isEmpty(lang)) { String[] list = lang.split("_"); if (list.length == 2) { locale = new Locale(list[0], list[1]); } } return locale; } @Override public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) { } }
启用自定义国际化处理器
package com.example.demo.config; import com.example.demo.component.MyLocaleResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; /** * 配置首页视图 */ @Configuration public class CustomVmcConfig extends WebMvcConfigurerAdapter { @Bean public MyLocaleResolver localeResolver() { return new MyLocaleResolver(); } }
优先获取查询参数返回语言设置
http://localhost:8080/?lang=zh_CN http://localhost:8080/?lang=en_US
登陆&拦截器
开发期间模板引擎修改要实时生效
- 禁用模板引擎缓存
- 重新编译
<!--登录错误消息提示--> <p style="color: red;" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}" ></p>
拦截器进行登录检查
登录
package com.example.demo.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 javax.servlet.http.HttpSession; import java.util.Map; @Controller public class LoginController { @PostMapping("/user/login") // 等价于 @RequestMapping(value = "/user/login", method = {RequestMethod.POST}) public String login(@RequestParam("username") String username, @RequestParam("password") String password, Map<String, Object> map, HttpSession session ) { if (!StringUtils.isEmpty(username) && "123".equals(password)) { session.setAttribute("loginUser", username); // 登录成功 防止表单重新提交,做一个重定向 return "redirect:/dashboard.html"; } else { // 登录失败 map.put("msg", "账号或密码不正确"); return "login"; } } }
拦截器
package com.example.demo.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 loginUser = request.getSession().getAttribute("loginUser"); // 未登录,返回登录页面 if (loginUser == 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 { } }
注册拦截器
package com.example.demo.config; import com.example.demo.component.LoginHandlerInterceptor; import com.example.demo.component.MyLocaleResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; /** * 配置首页视图 */ @Configuration @SuppressWarnings("all") public class CustomVmcConfig extends WebMvcConfigurerAdapter { // 设置首页位置,默认访问 public/index.html 没有经过模板引擎处理 @Bean public WebMvcConfigurerAdapter CustomVmcConfig() { WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() { /** * 注册视图控制器 * @param registry */ @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("login"); registry.addViewController("/index.html").setViewName("login"); registry.addViewController("/dashboard.html").setViewName("dashboard"); } /** * 注册拦截器 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { // super.addInterceptors(registry); // 拦截任意路径下的所有请求, 排除请求 registry.addInterceptor(new LoginHandlerInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/index.html", "/", "/user/login", "/static/**"); } }; return adapter; } }
Restful CRUD
Rest 风格
URI: /资源/资源标识 HTTP 请求方式区分对资源 CRUD
说明 | 普通 CRUD(URI 区分操作) | RestfulCRUD |
---|---|---|
查询 | getEmp | GET emp |
添加 | addEmp | POST emp |
修改 | updateEmp?id=1 | PUT emp/{id} |
删除 | deleteEmp?id=1 | DELETE emp/{id} |
查询接口定义
说明 | 请求方式 | 请求 URI |
---|---|---|
查询所有员工 | GET | emps |
查询某个员工 | GET | emp/{id} |
添加页面 | GET | emp |
添加员工 | POST | emp |
修改页面 | GET | emp/{id} |
修改员工 | PUT | emp |
删除员工 | DELETE | emp/{id} |
员工列表-公共页抽取
公共片段抽取
<!-- 1、抽取公共片段 --> <div th:fragment="copy">content</div> <!-- 2、引入公共片段 --> <div th:insert="~{footer::copy}">content</div> <!-- 3、默认效果 --> <!-- insert功能片段在div标签中 -->
~{templateName::selector} 模板名::选择器 ~{templateName::fragmentName}模板名::片段名
3 种方式引入片段
th:insert 插入 th:replace 替换 th:include 引入片段内容 使用th:insert可以不写~{} 转义[[~{}]] 不转义[(~{})]
引入片段时候传入参数
链接高亮&列表完成
redirect 重定向
forward 转发
日期格式化
spring.mvc.format.date: yyyy-MM-dd
HiddenHttpMethodFilter
SpringMVC 中配置 HiddenHttpMethodFilter
页面创建一个 Post 表单
创建一个 input 项 name="_method",值就是请求方式
<input type="hidden" name="_method" value="put" th:if="${emp!=null}" />
有些版本可能需要配置
application.yml
spring.mvc.hiddenmethod.filter.enabled = true
错误页面
ErrorMvcAutoConfiguration
有模板引擎的情况下
error/状态码 eg: 精确匹配 error/404.html 模糊匹配 error/4xx.html error/5xx.html
自定义异常处理
1、返回 JSON 数据
package com.example.demo.controller; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import java.util.HashMap; import java.util.Map; @ControllerAdvice public class ExceptionController { @ResponseBody @ExceptionHandler public Map<String, Object> handleException(Exception e){ Map<String, Object> map = new HashMap<>(); map.put("code", -1); map.put("msg", e.getMessage()); return map; } }
{ "msg": "用户不存在", "code": -1 }
嵌入式 Servlet 容器配置修改
Tomcat
通用配置
# servlet配置 server.port=8001 server.context-path=/demo # Tomcat配置 server.tomcat.uri-encoding=UTF-8
注册 servlet 三大组件
Servlet/Filter/Listener
1、Servlet
package com.example.demo.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().println("MyServlet"); } }
2、Filter
package com.example.demo.filter; import javax.servlet.*; import java.io.IOException; public class MyFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("MyFilter"); chain.doFilter(request, response); } }
3、Listener
package com.example.demo.listener; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; public class MyListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { System.out.println("contextInitialized"); } @Override public void contextDestroyed(ServletContextEvent sce) { System.out.println("contextDestroyed"); } }
注册组件
package com.example.demo.config; import com.example.demo.filter.MyFilter; import com.example.demo.listener.MyListener; import com.example.demo.servlet.MyServlet; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Arrays; /** * 注册Servlet/Filter/Listener组件 */ @Configuration public class MyServletConfig { @Bean public ServletRegistrationBean myServlet(){ ServletRegistrationBean registrationBean = new ServletRegistrationBean(); registrationBean.setServlet(new MyServlet()); registrationBean.setUrlMappings(Arrays.asList("/servlet")); return registrationBean; } @Bean public FilterRegistrationBean myFilter(){ FilterRegistrationBean registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(new MyFilter()); registrationBean.setUrlPatterns(Arrays.asList("/servlet")); return registrationBean; } @Bean public ServletListenerRegistrationBean myListener(){ ServletListenerRegistrationBean registrationBean = new ServletListenerRegistrationBean(new MyListener()); return registrationBean; } }
访问
http://localhost:8080/servlet
默认拦截: /
修改 server.servletPath
其他 Servlet 容器
Tomcat 默认
Jetty 长连接
Undertow 不支持 jsp
嵌入式容器
外部容器
jar 包:执行 SpringBoot 主类的 main 方法,启动 IOC 容器,创建嵌入式的 Servlet 容器
war 包:启动服务器,服务器启动 SpringBoot,启动 IOC 容器