04_SpringBoot 与 web 开发

SpringBoot 与 web 开发

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

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 使用
  1. 只要把我们的 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";
}
  1. 导入Thymeleaf的名称空间

    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    
  2. 使用Thymeleaf的语法

4. 语法规则
  1. th:text 改变div中的文本内容

    th:属性 改变任意原生属性的值

在这里插入图片描述

  1. 表达式
${...}:获取变量值;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的默认配置 :

  1. 自动配置了视图解析器 : 根据方法的返回值得到视图对象,视图对象决定如何渲染(转发,重定向)

    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;
        }
    }
    
  2. 静态资源文件夹路径,webjars

  3. 静态首页访问

  4. 自动注册了转换器(Convertor 类型转换)和格式化器(Formatter 格式化日期,国际化等) 自定义的格式化转换器,只需要放在容器中即可.

    Convertor : 例如对象映射

    Formatter : 格式化日期

  5. HttpMessageConvertor : SpringMVC用来转换http请求和响应的 : User —> Json

    获取所有的HttpMessageConvertor 只需要将自己的组件注册到容器中 (@Bean @Component)

  6. MessageCodesResolver : 定义错误代码生成规则

  7. 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 全面接管?

  1. EnableWebMvc
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
  1. DelegatingWebMvcConfiguration
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
  1. 注意上面继承了 WebMvcConfigurationSupport

  2. 我们看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";
    }
}
国际化 :

​ 目的 : 通过按钮切换国际化内容

  1. 在 resources 中新建一个文件夹 i18n

  2. 然后在里面放入国际化信息

    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
    
  3. 在配置文件中配置国际化的地址

    application.properties

    # 配置国际化位置
    spring.messages.basename=i18n.login
    
    1. 创建一个区域处理器 用来处理区域信息
    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) {
        }
    }
    
  4. 创建一个配置类 将自定义的区域处理器放入容器中

@Configuration
public class MyConfig extends WebMvcConfigurerAdapter {
    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }
}
  1. 页面中请求和获取

    请求 :

    ​ 在 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>
    
  2. 测试

登录 :
  1. 在页面中的 form 表单中添加 action 和 method 为 post 方式.
<form class="form-signin" th:action="@{/user/login}" method="post">
  1. 为要提交的表单项添加 name 属性与方法的形参进行映射.
<input type="text" name="username">
<input type="password" name="password">
  1. 编写 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";
    }
}
  1. 修改 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";
    }
}
  1. 通过添加拦截器用来验证用户是否存在.
  • 修改 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 - 员工列表 :

要求 :

  1. 满足 RESTful 风格

    URI

    普通 CRUDRESTFul 风格 CRUD
    查询getUseruser — GET
    增加addUseruser — POST
    修改updateUser?id=xxx&…user/id — PUT
    删除deleteUser?id=xxxuser/id — DELETE
  2. 要求的 URI

    请求的URI请求方式
    查询所有员工empsGET
    查询某个员工(来到修改页面)emp/{id}GET
    来到添加页面empGET
    添加某个员工empPOST
    来到修改页面(查询并回显)emp/{id}GET
    修改某个员工empPUT
    删除某个员工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 如何进行自动配置 :

​ 四个组件 :

  1. ErrorPageCustomizer:
//系统出现错误以后来到error请求进行处理;(web.xml注册的错误页面规则)
@Value("${error.path:/error}")
private String path = "/error";  
  1. 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);
    }
  1. 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);
}
  1. 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 容器自动配置原理 :

步骤 :

  1. 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();
        }
    }
  1. 容器中添加某个组件要创建对象后置处理器就开始执行 ,[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;
}
  1. 后置处理器获取容器中所有的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 ?

步骤 :

  1. SpringBoot 启动执行 run 方法.
  2. refresh SpringBoot 刷新容器 (初始化容器,并创建组件),如果是web应用创建AnnotationConfigEmbeddedWebApplicationContext,否则创建AnnotationConfigApplicationContext.
  3. 进入 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();
      }
   }
}
  1. 在上面的refresh方法中调用onRefresh()方法

  2. web应用的容器会创建嵌入式的Servlet容器:createEmbeddedServletContainer();

  3. 获取嵌入式的Servlet容器工厂:

    EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();

    从ioc容器中获取EmbeddedServletContainerFactory 组件;TomcatEmbeddedServletContainerFactory创建对象,后置处理器一看是这个对象,就获取所有的定制器来先定制Servlet容器的相关配置;

  4. 使用容器工厂获取嵌入式的Servlet容器

    this.embeddedServletContainer = containerFactory.getEmbeddedServletContainer(getSelfInitializer());

  5. 嵌入式的Servlet容器创建对象并启动Servlet容器;

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

    IOC容器启动创建嵌入式的Servlet容器

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值