SpringBoot 与 web 开发
1. 怎么使用 Springboot 创建 web 项目?
- 创建 SpringBoot 应用, 选中我们需要的模块
- SpringBoot 已经将这些场景配置好了,只需要在配置文件中指定少量配置就可以运行起来
- 自己编写业务代码
2. 自动配置原理?
这个场景 SpringBoot 帮我们配置了什么? 能不能修改? 能修改哪些配置? 能不能扩展?
...AutoConfiguration : 帮我们给容器中自动配置组件
...Properties : 配置来封装配置文件的内容
3. SpringBoot 对静态资源的映射规则?
@Override //资源添加映射器
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache()
.getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry
.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META- INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod))
.setCacheControl(cacheControl));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(
registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(
this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod))
.setCacheControl(cacheControl));
}
}
映射规则一 :
所有的 /webjars/** 都去 classpath:/META-INF/resources/webjars/ 下寻找资源.
什么是webjars? 以 jar 包的形式引入静态资源.
例子 : 引入 jQuery
1. 到 webjars 官网寻找需要的jQuery
2. 将所需的 jQuery 的 maven 引入到 pom 文件中
3. 然后我们观察引入后的jar包
4. 访问 bower.json
映射规则二 :
/** 访问当前项目的任意资源(静态资源文件夹)
"classpath:/META-INF/resources",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
"/" : 当前项目的根路径
注意 : classpath 的路径是 java文件夹 和 resources文件夹 都是该路径
映射规则三 :
@Bean //欢迎页的设置.
public WelcomePageHandlerMapping welcomePageHandlerMapping(
ApplicationContext applicationContext) {
return new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext),
applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
}
被 /** 映射为在静态资源文件夹下的index.html
如 : localhost:8080/
映射规则四 :
自定义图标
public static class FaviconConfiguration implements ResourceLoaderAware {
private final ResourceProperties resourceProperties;
private ResourceLoader resourceLoader;
public FaviconConfiguration(ResourceProperties resourceProperties) {
this.resourceProperties = resourceProperties;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Bean
public SimpleUrlHandlerMapping faviconHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",
faviconRequestHandler()));
return mapping;
}
@Bean
public ResourceHttpRequestHandler faviconRequestHandler() {
ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
requestHandler.setLocations(resolveFaviconLocations());
return requestHandler;
}
private List<Resource> resolveFaviconLocations() {
String[] staticLocations = getResourceLocations(
this.resourceProperties.getStaticLocations());
List<Resource> locations =
new ArrayList<>(staticLocations.length + 1);
Arrays.stream(staticLocations).map(this.resourceLoader::getResource)
.forEach(locations::add);
locations.add(new ClassPathResource("/"));
return Collections.unmodifiableList(locations);
}
}
}
所有的 **/favicon.ico 都是在静态资源文件下找.
4. 遇到的问题
谷歌浏览器缓存问题 清理缓存的快捷键
CTRL+SHIFT+DEL:直接进入“清除浏览数据”页面,包括清除浏览历史记录、清空缓存、删除Cookie等。
5. 模板引擎
1. 简介
常用的模板引擎 :JSP Velocity Freemarker Thymeleaf
示意图 :
SpringBoot 推荐使用 Thymeleaf :
语法更简单且功能更强大
2. 引入Thymeleaf
引入Thymeleaf的starters
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
修改 Thymeleaf 版本
<properties>
<thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
<!-- 布局功能支持程序, Thymeleaf3主程序 需要 layout2 以上版本 -->
<thymeleaf.layout-dialect.version>2.4.1</thymeleaf.layout-dialect.version>
</properties>
3. Thymeleaf 使用
-
只要把我们的 html 页面放在 classpath:/templates/ ,Thymeleaf 就能自动渲染.
@ConfigurationProperties(prefix = "spring.thymeleaf") public class ThymeleafProperties { private static final Charset DEFAULT_ENCODING = Charset.forName("UTF-8"); private static final MimeType DEFAULT_CONTENT_TYPE = MimeType.valueOf("text/html"); public static final String DEFAULT_PREFIX = "classpath:/templates/"; public static final String DEFAULT_SUFFIX = ".html";
@RequestMapping("/hello")
public String hello(){
//转到 classpath/templates/success.html
return "success";
}
-
导入Thymeleaf的名称空间
<html lang="en" xmlns:th="http://www.thymeleaf.org">
-
使用Thymeleaf的语法
4. 语法规则
-
th:text 改变div中的文本内容
th:属性 改变任意原生属性的值
- 表达式
${...}:获取变量值;OGNL;
1)获取对象的属性、调用方法
2)使用内置的基本对象:
ctx : the context object.
vars: the context variables.
locale : the context locale.
request : (only in Web Contexts) the HttpServletRequest object.
response : (only in Web Contexts) the HttpServletResponse object.
session : (only in Web Contexts) the HttpSession object. eg: ${session.foo}
servletContext : (only in Web Contexts) the ServletContext object.
3)内置的一些工具对象:
execInfo : information about the template being processed.
messages : methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax.
uris : methods for escaping parts of URLs/URIs
conversions : methods for executing the configured conversion service (if any).
dates : methods for java.util.Date objects: formatting, component extraction, etc.
calendars : analogous to #dates , but for java.util.Calendar objects.
numbers : methods for formatting numeric objects.
strings : methods for String objects: contains, startsWith, prepending/appending
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).
*{...}:选择表达式:和${}在功能上是一样;
补充:配合 th:object="${session.user}:
<div th:object="${session.user}">
<p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>
#{...}:获取国际化内容
Link URL Expressions: @{...}:定义URL;
@{/order/process(execId=${execId},execType='FAST')}
Fragment Expressions: ~{...}:片段引用表达式
<div th:insert="~{commons :: main}">...</div>
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: _
6. SpringMVC 的自动配置
SpringBoot自动配置好了SpringMVC
以下是SpringBoot对SpringMVC的默认配置 :
-
自动配置了视图解析器 : 根据方法的返回值得到视图对象,视图对象决定如何渲染(转发,重定向)
ContentNegotiatingViewResolver : 组合所有的视图解析器
如何定制 : 我们可以在容器中添加一个视图解析器,ContentNegotiatingViewResolver 会自动将其组合进来.
@SpringBootApplication public class Springboot05RestApplication { public static void main(String[] args) { SpringApplication.run(Springboot05RestApplication.class, args); } @Bean public ViewResolver myViewResolver(){ return new MyViewResolver(); } } class MyViewResolver implements ViewResolver{ @Override public View resolveViewName(String s, Locale locale) throws Exception { return null; } }
-
静态资源文件夹路径,webjars
-
静态首页访问
-
自动注册了转换器(Convertor 类型转换)和格式化器(Formatter 格式化日期,国际化等) 自定义的格式化转换器,只需要放在容器中即可.
Convertor : 例如对象映射
Formatter : 格式化日期
-
HttpMessageConvertor : SpringMVC用来转换http请求和响应的 : User —> Json
获取所有的HttpMessageConvertor 只需要将自己的组件注册到容器中 (@Bean @Component)
-
MessageCodesResolver : 定义错误代码生成规则
-
ConfigurableWebBindingInitializer :
初始化web数据绑定器 : 请求数据 —> JavaBean
我们可以配置一个ConfigurableWebBindingInitializer来替换默认的.
7. 如何修改SpringBoot的默认配置
模式 :
1. SpringBoot 在自动配置很多组件的时候,先看容器中有没有用户自己配置的,如果有就用用户配置的,如果没有,才自动配置,如果有些组件可以有多个将用户配置的和默认的组合起来
2. 在SpringBoot中会有非常多的 ...Configurer 帮助我们进行扩展配置
3. 在SpringBoot中会有很多的 ...Customizer 帮助我们进行定制配置
8. 扩展 SpringMVC
以前可以加入的配置 :
<mvc:view-controller path="/hello" view-name="success"/>
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/hello"/>
<bean></bean>
</mvc:interceptor>
</mvc:interceptors>
SpringBoot方式 :
创建一个配置类,标 @Configuration 注解, 继承 WebMvcConfigurerAdapter 类.
全面接管 SpringMVC :
在配置类中添加注解即可.
//使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能
@EnableWebMvc
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// super.addViewControllers(registry);
//浏览器发送 /atguigu 请求来到 success
registry.addViewController("/atguigu").setViewName("success");
}
}
为什么加入 @EnableWebMVC 全面接管?
- EnableWebMvc
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
- DelegatingWebMvcConfiguration
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
-
注意上面继承了 WebMvcConfigurationSupport
-
我们看WebMvcAutoConfiguration源码
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) : 当没有WebMvcConfigurationSupport时才将组件添加到容器中. 而当加入了 @EnableWebMVC 之后会继承 WebMvcConfigurationSupport 类,因此原来的自动配置失效.
9. SpringCRUD
默认访问首页 :
别忘了配置 Thymeleaf jar 包
@Controller
public class IndexController {
@RequestMapping({"/","/login"})
public String index(){
return "login";
}
}
国际化 :
目的 : 通过按钮切换国际化内容
-
在 resources 中新建一个文件夹 i18n
-
然后在里面放入国际化信息
login.properties
login.btn=登录 login.password=密码 login.remember=记住我 login.tip=请登录 login.username=用户名
login_zh_CH.properties
login.btn=登录 login.password=密码 login.remember=记住我 login.tip=请登录 login.username=用户名
login_en_US.properties
login.btn=Sign In login.password=Password login.remember=Remember me login.tip=Please sign in login.username=Username
-
在配置文件中配置国际化的地址
application.properties
# 配置国际化位置 spring.messages.basename=i18n.login
- 创建一个区域处理器 用来处理区域信息
public class MyLocaleResolver implements LocaleResolver { @Override public Locale resolveLocale(HttpServletRequest request) { String language = request.getParameter("language"); Locale locale = Locale.getDefault(); if(language != null){ if(!StringUtils.isEmpty(language)){ String[] split = language.split("_"); System.out.println(split[0]+split[1]); locale = new Locale(split[0],split[1]); } } return locale; } @Override public void setLocale(HttpServletRequest request, HttpServletResponse response,Locale locale) { } }
-
创建一个配置类 将自定义的区域处理器放入容器中
@Configuration
public class MyConfig extends WebMvcConfigurerAdapter {
@Bean
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}
}
-
页面中请求和获取
请求 :
在 url 后添加国际化参数 eg : zh_CN en_US …
<a class="btn btn-sm" th:href="@{/(language='zh_CH')}">中文</a> <a class="btn btn-sm" th:href="@{/(language='en_US')}">English</a>
通过 Thymeleaf 语法获取国际化信息 :
<form class="form-signin" action="dashboard.html"> <img class="mb-4" th:src="@{/asserts/img/bootstrap-solid.svg}"> <h1 th:text="#{login.tip}" class="h3 mb-3 font-weight-normal"></h1> <label th:text="#{login.username}" class="sr-only"></label> <input type="text" th:placeholder="#{login.username}"> <label class="sr-only">Password</label> <input type="password" th:placeholder="#{login.password}"> <div class="checkbox mb-3"> <label> <input type="checkbox" value="remember-me">[[#{login.remember}]] </label> </div> <button type="submit" th:text="#{login.btn}">Sign in</button> </form>
-
测试
登录 :
- 在页面中的 form 表单中添加 action 和 method 为 post 方式.
<form class="form-signin" th:action="@{/user/login}" method="post">
- 为要提交的表单项添加 name 属性与方法的形参进行映射.
<input type="text" name="username">
<input type="password" name="password">
- 编写 LoginController , 用来检测用户是否存在且是否正确.
@PostMapping("/user/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
Map<String,Object> map, HttpSession session){
if(password.equals("123456") && username.equals("admin")){
session.setAttribute("loginUser",username);
// 问题 : 这里使用的是请求转发,因此当刷新页面时会发生表单重复提交问题
return "main";
}else{
map.put("info","用户名或密码错误");
return "index";
}
}
- 修改 LoginController 返回的方法为重定向解决重复表单提交问题.
@PostMapping("/user/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password){
if(password.equals("123456") && username.equals("admin")){
// 解决 : 使用重定向解决表单重复提交问题
// 问题 : 但是当使用重定向之后 地址发生变化 用户可以通过地址直接到主页面
return "redirect:/main";
}else{
map.put("info","用户名或密码错误");
return "index";
}
}
- 通过添加拦截器用来验证用户是否存在.
- 修改 LoginController
@PostMapping("/user/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
Map<String,Object> map, HttpSession session){
if(password.equals("123456") && username.equals("admin")){
//解决步骤1 : 通过将登录对象放入 session 中
session.setAttribute("loginUser",username);
return "redirect:/main";
}else{
map.put("info","用户名或密码错误");
return "index";
}
}
- 新建一个登录拦截器类
/**
* 登录拦截器 检查用户是否登录
*/
public class LoginHandlerInterceptor implements HandlerInterceptor{
//目标方法执行之前
//解决步骤2 : 通过查看session中是否有loginUser对象即可
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object user = request.getSession().getAttribute("loginUser");
if(user == null){
request.setAttribute("info","您没有权限,请登录!");
//请求转发到 index 请求中
request.getRequestDispatcher("/index").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 {
}
}
- 在配置类中配置拦截器类
@Configuration
public class MyConfig extends WebMvcConfigurerAdapter {
//将区域处理类放入容器中
@Bean
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}
//解决步骤3 : 将登录拦截器放入容器中
@Override
public void addInterceptors(InterceptorRegistry registry) {
//不需要管静态资源 SpringBoot已经管理好了静态资源映射
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/index","/","/user/login");
}
}
CRUD - 员工列表 :
要求 :
-
满足 RESTful 风格
URI
普通 CRUD RESTFul 风格 CRUD 查询 getUser user — GET 增加 addUser user — POST 修改 updateUser?id=xxx&… user/id — PUT 删除 deleteUser?id=xxx user/id — DELETE -
要求的 URI
请求的URI 请求方式 查询所有员工 emps GET 查询某个员工(来到修改页面) emp/{id} GET 来到添加页面 emp GET 添加某个员工 emp POST 来到修改页面(查询并回显) emp/{id} GET 修改某个员工 emp PUT 删除某个员工 emp/{id} DELETE
修改页面 :
<a class="nav-link" th:href="@{/emps}">
员工列表
</a>
编写 EmpController :
@Controller
public class EmpController {
@Autowired
private EmployeeDao employeeDao;
@GetMapping("/emps")
public String getAll(Model model){
Collection<Employee> employees = employeeDao.getAll();
model.addAttribute("emps",employees);
//Thymeleaf 模板引擎会自动找 /templates/xxx/xxx.html
return "emp/list";
}
}
抽取出公共页面 :
抽取方式(两种) :
<!-- 第一种 : 通过 th:fragment="" 抽取 -->
<nav th:fragment="topbar">
</nav>
<!-- 第二种 : 通过设置 id 进行抽取 -->
<nav id="sidebar">
</nav>
引用 (三种):
<!-- 第一种 : 通过 th:replace="" 引用 -->
<!-- 引用后所引用的片段外面被 div 包裹 -->
<div th:replace="~{dashboard :: topbar}"></div>
<!-- 第二种 : 通过 th:insert="" 引用 -->
<!-- 引用后所引用的片段没有div 只是替换 -->
<div th:insert="~{dashboard :: topbar}"></div>
<!-- 第三种 : 通过 th:include="" 引用 -->
<!-- 只包含被引用片段中包含的内容 -->
<div th:include="~{dashboard :: topbar}"></div>
员工列表展示 :
controller
@GetMapping("/emps")
public String getAll(Model model){
Collection<Employee> employees = employeeDao.getAll();
model.addAttribute("emps",employees);
//Thymeleaf 模板引擎会自动找 /templates/xxx/xxx.html
return "emp/list";
}
html
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<button class="btn btn-sm btn-success">添加员工</button>
<div class="table-responsive">
<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>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}"></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>
CRUD -员工添加 :
到添加页面 :
controller
@GetMapping("/emp")
public String toAdd(Model model){
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("depts",departments);
return "emp/add";
}
页面
<form>
<div class="form-group">
<label>LastName</label>
<input type="text" class="form-control" placeholder="zhangsan">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" class="form-control" placeholder="zhangsan@atguigu.com">
</div>
<div class="form-group">
<label>department</label>
<select class="form-control">
<!-- 取出部门 -->
<option th:value="${dept.id}"
th:text="${dept.departmentName}" th:each="dept:${depts}">1</option>
</select>
</div>
<button type="submit" class="btn btn-primary">添加</button>
</form>
创建一个添加用户的页面 :
通过把 input 标签中加上 name 属性, 映射到页面的形参中.
<form th:action="@{/emp}" method="post">
<div class="form-group">
<label>LastName</label>
<input name="lastName" type="text">
</div>
<div class="form-group">
<label>Email</label>
<input name="email" type="email">
</div>
<div class="form-group">
<label>Gender</label><br/>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="1">
<label class="form-check-label">男</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="0">
<label class="form-check-label">女</label>
</div>
</div>
<div class="form-group">
<label>department</label>
<select class="form-control" name="department.id">
<option th:value="${dept.id}" th:text="${dept.departmentName}" th:each="dept:${depts}">1</option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input name="birth" type="text" class="form-control" placeholder="zhangsan">
</div>
<button type="submit" class="btn btn-primary">添加</button>
</form>
controller
@PostMapping("/emp")
public String add(Employee employee){
employeeDao.save(employee);
return "redirect:/emps";
}
CRUD -员工修改 :
回显 :
Controller :
@GetMapping("/emp/{id}")
public String toEdit(@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";
}
页面通过 th:value 属性出结果.
修改 :
页面 :
<form th:action="@{/emp}" method="post">
<input type="hidden" name="id" th:value="${emp.id}" th:if="${emp!=null}"/>
<!-- 通过添加隐藏域提交方式为PUT -->
<input type="hidden" name="_method" value="put" th:if="${emp!=null}"/>
controller :
@PutMapping("/emp")
public String edit(Employee employee){
System.out.println(employee.toString());
employeeDao.save(employee);
return "redirect:/emps";
}
CRUD -员工删除:
页面 :
<!-- 通过在 button 按钮使用 th:att="k1=v1" 来设置属性的值 -->
<button th:attr="del_uri=@{/emp/}+${emp.id}" class="btn btn-sm btn-danger del-btn">删除</button>
<!-- 通过一个表单来发起请求 -->
<form id="deleteForm" method="post">
<input type="hidden" name="_method" value="delete">
</form>
<!-- 通过 js 代码进行表单的提交 -->
<script>
$(".del-btn").click(function () {
alert("aaa");
$("#deleteForm").attr("action",$(this).attr("del_uri")).submit();
});
</script>
注意 : 要把 js 代码写在引入 jQuery 的后面…
controller :
@DeleteMapping("emp/{id}")
public String delete(@PathVariable("id") Integer id){
employeeDao.delete(id);
return "redirect:/emps";
}
10. Springboot 自动处理机制
默认的处理机制 :
如果是浏览器请求发生错误 : 返回错误页面
如果是客户端请求发生错误 : 返回json数据
{
"timestamp": "2019-04-30T00:00:24.541+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/123"
}
如何判断客户端与浏览器呢 ?
客户端 :
浏览器 :
SpringBoot 如何进行自动配置 :
四个组件 :
- ErrorPageCustomizer:
//系统出现错误以后来到error请求进行处理;(web.xml注册的错误页面规则)
@Value("${error.path:/error}")
private String path = "/error";
- BasicErrorController :
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
//产生html类型的数据;浏览器发送的请求来到这个方法处理
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//去哪个页面作为错误页面;包含页面地址和页面内容
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}
//产生json数据,其他客户端来到这个方法处理;
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}
- DefaultErrorViewResolver :
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//默认SpringBoot可以去找到一个页面? error/404
String errorViewName = "error/" + viewName;
//模板引擎可以解析这个页面地址就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
//模板引擎可用的情况下返回到errorViewName指定的视图地址
return new ModelAndView(errorViewName, model);
}
//模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面 error/404.html
return resolveResource(errorViewName, model);
}
- DefaultErrorAttributes :
//帮我们在页面共享信息;
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, requestAttributes);
addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
addPath(errorAttributes, requestAttributes);
return errorAttributes;
}
流程 : 当系统出现错误如 4xx , 5xx 等, ErrorPageCustomizer 请求 /error 请求, 然后会被BasicErrorController 进行处理, 然后通过 DefaultErrorAttributes 的 getErrorAttributes 帮我们在页面共享信息, 通过DefaultErrorViewResolver 决定去哪个页面.
如何自定义错误响应 :
自定义错误页面 :
1. 有模板引擎的情况下 : 将错误页面以 错误代码.html 放入模板引擎文件夹下的 error文件夹下, 当发生响应错误码的时候会自动跳转到响应的页面.
我们也可以使用 4xx 或者 5xx 来命名错误页面,当发生错误时,优先寻找精确的错误页面.
页面能获取的信息 :
timestamp:时间戳
status:状态码
error:错误提示
exception:异常对象
message:异常消息
errors:JSR303 数据校验的错误都在这里
2. 没有模板引擎 :(模板引擎找不到这个错误页面),静态资源文件夹下找;
3. 如果以上都没有找到,则使用 springboot 默认的页面.
自定义错误的json数据 :
测试 :
controller :
@RequestMapping("/hello")
public String hello(String user){
if(user == null){
throw new UserNotFoundException();
}
return "helloworld";
}
UserNotFoundException :
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException() {
super("用户不存在!");
}
}
方式一 : 自定义异常处理器 : MyExceptionHandler
@ControllerAdvice
public class MyExceptionHandler {
@ResponseBody
@ExceptionHandler(UserNotFoundException.class)
public Map<String, Object> exceptionHandler(Exception e){
Map<String,Object> map = new HashMap<>();
map.put("code","userNotExist");
map.put("message",e.getMessage());
return map;
}
}
进行测试 : localhost:8080/hello
页面效果 :
postman效果 :
问题 : 没有自适应性(即客户端和浏览器都返回的是json数据)
方式二 : 转到 /error 进行实现自适应效果
@ExceptionHandler(UserNotFoundException.class)
public String exceptionHandler(Exception e, HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
//传入我们自己的错误状态码 4xx 5xx,否则就不会进入定制错误页面的解析流程
request.setAttribute("javax.servlet.error.status_code",400);
map.put("code","userNotExist");
map.put("message",e.getMessage());
return "forward:/error";
}
浏览器效果 :
postman效果 :
问题 : 可以到自定义的页面,但是没有自定义的json数据
方式三 : 将我们的定制数据携带出去并且进入自定义页面
出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由getErrorAttributes得到的(是AbstractErrorController(ErrorController)规定的方法);
1、完全来编写一个ErrorController的实现类【或者是编写AbstractErrorController的子类】,放在容器中;
2、页面上能用的数据,或者是json返回能用的数据都是通过errorAttributes.getErrorAttributes得到;
容器中DefaultErrorAttributes.getErrorAttributes();默认进行数据处理的;
MyExceptionHandler
@ExceptionHandler(UserNotFoundException.class)
public String exceptionHandler(Exception e, HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
//传入我们自己的错误状态码 4xx 5xx,否则就不会进入定制错误页面的解析流程
request.setAttribute("javax.servlet.error.status_code",500);
map.put("code","userNotExist");
map.put("message",e.getMessage());
request.setAttribute("ext",map);
return "forward:/error";
}
自定义 ErrorAttribute
@Component
public class ErrorAttribute extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest,
boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(webRequest,
includeStackTrace);
map.put("name","lifanyu");
Map<String,Object> ext =
(Map<String, Object>) webRequest.getAttribute("ext",0);
map.put("ext",ext);
return map;
}
}
页面
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<h1>status:[[${status}]]</h1>
<h1>timestamp:[[${timestamp}]]</h1>
<h1>message:[[${ext.message}]]</h1>
<h1>code:[[${ext.code}]]</h1>
<h1>name:[[${name}]]</h1>
</main>
效果 :
页面 :
postman :
掌握
11. 配置嵌入式 servlet 容器
SpringBoot默认使用的是嵌入式的servlet容器 : tomcat.
如何定制和修改定制嵌入式servlet容器的相关配置?
方法一 : 在配置文件中修改和server有关的配置
server.port=8081
方法二 : 编写一个 servlet 容器的定制器 WebServerFactoryCustomizer ,放入容器中
@Bean
public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer(){
return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() {
@Override
public void customize(ConfigurableWebServerFactory factory) {
factory.setPort(8084);
}
};
}
注册三大组件 :
注册 servlet :
编写servlet
public class Myservlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("helloworld");
}
}
将servlet注入到容器中
@Bean
public ServletRegistrationBean myServlet(){
//参数二当请求 /aa 时执行该 servlet
return new ServletRegistrationBean(new Myservlet(),"/aa");
}
注册 listener :
编写 listener
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("应用启动了...");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("应用销毁了...");
}
}
注册到容器
@Bean
public ServletListenerRegistrationBean myListener(){
ServletListenerRegistrationBean<MyListener> myListenerServletListenerRegistrationBean =
new ServletListenerRegistrationBean<>(new MyListener());
return myListenerServletListenerRegistrationBean;
}
注册 filter :
编写filter
public class MyFilter extends HttpFilter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("filter process...");
chain.doFilter(request,response);
}
}
注册到容器
@Bean
public FilterRegistrationBean myFilter(){
FilterRegistrationBean filter = new FilterRegistrationBean();
filter.setFilter(new MyFilter());
filter.setUrlPatterns(Arrays.asList("/index","/cc"));
return filter;
}
切换其他嵌入式容器 :
步骤 :
排除要切换的servlet容器
加入切换成的servlet容器
默认使用tomcat
切换 Jetty :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<artifactId>spring-boot-starter-jetty</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>
切换 UnderTow :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<artifactId>spring-boot-starter-undertow</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>
嵌入式 servlet 容器自动配置原理 :
步骤 :
- SpringBoot根据导入的情况添加相应的 EmbeddedServletContainerFactory ,例如 :[TomcatEmbeddedServletContainerFactory]
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication
@Import(BeanPostProcessorsRegistrar.class)
//导入BeanPostProcessorsRegistrar:Spring注解版;给容器中导入一些组件
//导入了EmbeddedServletContainerCustomizerBeanPostProcessor:
//后置处理器:bean初始化前后(创建完对象,还没赋值赋值)执行初始化工作
public class EmbeddedServletContainerAutoConfiguration {
@Configuration
@ConditionalOnClass({ Servlet.class, Tomcat.class })//判断当前是否引入了Tomcat依赖;
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)//判断当前容器没有用户自己定义EmbeddedServletContainerFactory:嵌入式的Servlet容器工厂;作用:创建嵌入式的Servlet容器
public static class EmbeddedTomcat {
@Bean
public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory();
}
}
/**
* Nested configuration if Jetty is being used.
*/
@Configuration
@ConditionalOnClass({ Servlet.class, Server.class, Loader.class,
WebAppContext.class })
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedJetty {
@Bean
public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
return new JettyEmbeddedServletContainerFactory();
}
}
/**
* Nested configuration if Undertow is being used.
*/
@Configuration
@ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedUndertow {
@Bean
public UndertowEmbeddedServletContainerFactory undertowEmbeddedServletContainerFactory() {
return new UndertowEmbeddedServletContainerFactory();
}
}
- 容器中添加某个组件要创建对象后置处理器就开始执行 ,[EmbeddedServletContainerCustomizerBeanPostProcessor],只要是 servlet 容器工厂,就起作用.
//初始化之前
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
//如果当前初始化的是一个ConfigurableEmbeddedServletContainer类型的组件
if (bean instanceof ConfigurableEmbeddedServletContainer) {
//
postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer) bean);
}
return bean;
}
private void postProcessBeforeInitialization(
ConfigurableEmbeddedServletContainer bean) {
//获取所有的定制器,调用每一个定制器的customize方法来给Servlet容器进行属性赋值;
for (EmbeddedServletContainerCustomizer customizer : getCustomizers()) {
customizer.customize(bean);
}
}
private Collection<EmbeddedServletContainerCustomizer> getCustomizers() {
if (this.customizers == null) {
// Look up does not include the parent context
this.customizers = new ArrayList<EmbeddedServletContainerCustomizer>(
this.beanFactory
//从容器中获取所有这葛类型的组件:EmbeddedServletContainerCustomizer
//定制Servlet容器,给容器中可以添加一个EmbeddedServletContainerCustomizer类型的组件
.getBeansOfType(EmbeddedServletContainerCustomizer.class,
false, false)
.values());
Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE);
this.customizers = Collections.unmodifiableList(this.customizers);
}
return this.customizers;
}
- 后置处理器获取容器中所有的EmbeddedServletContainerCustomizer,调用定制器的定制方法.
@Override
public EmbeddedServletContainer getEmbeddedServletContainer(
ServletContextInitializer... initializers) {
//创建一个Tomcat
Tomcat tomcat = new Tomcat();
//配置Tomcat的基本环节
File baseDir = (this.baseDirectory != null ? this.baseDirectory
: createTempDir("tomcat"));
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
//将配置好的Tomcat传入进去,返回一个EmbeddedServletContainer;并且启动Tomcat服务器
return getTomcatEmbeddedServletContainer(tomcat);
}
嵌入式的servlet容器启动过程 :
什么时候创建嵌入式的Servlet容器工厂?什么时候获取嵌入式的Servlet容器并启动Tomcat ?
步骤 :
- SpringBoot 启动执行 run 方法.
- refresh SpringBoot 刷新容器 (初始化容器,并创建组件),如果是web应用创建AnnotationConfigEmbeddedWebApplicationContext,否则创建AnnotationConfigApplicationContext.
- 进入 AnnotationConfigEmbeddedWebApplicationContext 的 refresh() 方法
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
-
在上面的refresh方法中调用onRefresh()方法
-
web应用的容器会创建嵌入式的Servlet容器:createEmbeddedServletContainer();
-
获取嵌入式的Servlet容器工厂:
EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();
从ioc容器中获取EmbeddedServletContainerFactory 组件;TomcatEmbeddedServletContainerFactory创建对象,后置处理器一看是这个对象,就获取所有的定制器来先定制Servlet容器的相关配置;
-
使用容器工厂获取嵌入式的Servlet容器:
this.embeddedServletContainer = containerFactory.getEmbeddedServletContainer(getSelfInitializer());
-
嵌入式的Servlet容器创建对象并启动Servlet容器;
先启动嵌入式的Servlet容器,再将ioc容器中剩下没有创建出的对象获取出来;
IOC容器启动创建嵌入式的Servlet容器