2020:0628--06--SpringBoot与Web开发(2)

399 阅读14分钟

1. 引入资源

    1.  引入静态html页面资源

    注意资源的添加位置:
        ThymeleafProperties有Thymeleaf具体的配置规则:
        默认的前后缀:
            classpath:/templates/
            .html
            
        所以把html页面放到classpath:/templates/,Thymeleaf就会自动帮我们将
        字符串返回值。解析成:classpath:/templates/success.html页面。
        
    所以将静态html页面放在:

    2.  引入实体类和dao层

2. 默认访问首页和国际化功能

    1.  第一个要求:默认访问到首页。
        
        根据SpringBoot对静态资源的映射规则我们可以知道:
        
            /**访问路径下,都会到这些文件夹下找内容:
                "classpath:/META-INF/resources/", 
                "classpath:/resources/",
                "classpath:/static/", 
                "classpath:/public/" 
                "/":当前项目的根路径
        
        所以我们当前项目的默认访问首页是classpath:/static/index.html,不是
        classpath:/templates/index.html。

2.1 实现默认访问classpath:/templates/login.html

    1.  在controller中写一个方法,返回值是login
        @RequestMapping("/login")
        public String login(){
            return "login";
        }
        Thymeleaf会根据其具体的配置规则,将返回的字符串解析成:
        classpath:/templates/login.html

    2.  可以通过扩展SpringMVC的自动配置来添加一个视图映射:
    
        如果仅仅只是访问页面,不传数据的话,就可以配置一个试图映射,
        不用在controller在写空方法了。
        
        写法1:
        //所以的WebMvcConfigurer组件都会一起起作用
        @Bean //将组件注册到容器中
        public WebMvcConfigurer webMvcConfigurer(){
            WebMvcConfigurer configurer = new WebMvcConfigurer(){
                @Override
                public void addViewControllers(ViewControllerRegistry registry) {
                    registry.addViewController("/").setViewName("login");
                    registry.addViewController("/index.html").setViewName("login");
                }
            };
            return configurer;
        }
        写法2:
        @Configuration
        public class MyConfig implements WebMvcConfigurer {
            //注意2.0以上不再实现WebMvcConfigurerAdapter
        
            //添加一个自定义的视图映射
            @Override
            public void addViewControllers(ViewControllerRegistry registry) {
                //定义规则:哪些请求访问哪些页面。
                registry.addViewController("/login").setViewName("login");
            }
        }
    3.  将静态页面中引入的公共资源,通过webjars引入:
        
        1.将此处的bootstrap通过webjars引入到项目:

            <dependency>
                <groupId>org.webjars</groupId>
                <artifactId>bootstrap</artifactId>
                <version>4.5.0</version>
            </dependency>
        2.将模版引擎的代码提示引入到资源表头:
        html xmlns:th="http://www.thymeleaf.org

        3.用模版引擎的表达式将bootstrap引入到资源中
        
        SpringBoot对webjars静态资源的映射规则:
        所有/webjars/**请求, 都去classpath:/META-INF/resources/webjars/找资源.
        
        所以我们用Thymleaf语法中的th表达式语法将webjars引入到静态资源中:
        
        th:href@{...} 将原先元素中的href属性替换掉。

        发现原来login.html源码中的href属性被th:href="@{}"给替换了。    

        4.  再加一个项目路径
        
            在主配置文件中加一个项目路径:
            
            server.servlet.context-path=/crud

2.2 实现登录页面的国际化

    即通过浏览器的语言信息来动态显示页面的国际化效果。
    
    1. 以前的SpringMVC实现步骤
        1. 编写国际化配置文件
        2.使用ResourceBundleMessageSource管理国际化资源文件
        3.在页面使用fmt:message取出国际化
    
    2. SpringBoot步骤
    
        1.编写国际化配置文件,抽取页面需要显示的国际化消息。
        
            login.propertise
            login_zh.CN.propertise
            
            此时SpringBoot会发现我们想要做国际化配置文件,SpringBoot帮我们切换到
            国际化视图。会将我们刚才写的两个配置文件加到Resource Bundle 'login'目录下:

            此时我们在添加一个英文的国际化文件:就会有一个
            Add Properties Files to Resources Bundle选项
            
            我们按照如下步骤,就会得到一个英文的国际化文件。

        2. 编写配置
        
            为相关显示信息配置国际化

            SpringBoot提供的了一个国际化的配置视图:

            配置文件编写好了:最后得到效果

    3.  SpringBoot自动配置好了管理国际化资源文件的组件。
    
        MVC时还需要使用ResourceBundleMessageSource管理国际化资源文件。SpringBoot
    为我们配置好了相应的这个配置。
        
        相应的自动配置类是:MessageSourceAutoConfiguration。源码中正好有相关类的
    实例化:new ResourceBundleMessageSource()

    4.  既然自动配置好了,那么我们写的配置文件就要满足自动配置的条件,这样自动配置
        才能生效。
        
        分析一下源码:
public class MessageSourceAutoConfiguration {

	private static final Resource[] NO_RESOURCES = {};

	@Bean
	@ConfigurationProperties(prefix = "spring.messages")
	public MessageSourceProperties messageSourceProperties() {
		return new MessageSourceProperties();
	}

	@Bean
	public MessageSource messageSource(MessageSourceProperties properties) {
		ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
		if (StringUtils.hasText(properties.getBasename())) {
			messageSource.setBasenames(StringUtils
					.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
		}
		if (properties.getEncoding() != null) {
			messageSource.setDefaultEncoding(properties.getEncoding().name());
		}
		messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
		Duration cacheDuration = properties.getCacheDuration();
		if (cacheDuration != null) {
			messageSource.setCacheMillis(cacheDuration.toMillis());
		}
		messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
		messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
		return messageSource;
	}
        1.设置国际化配置文件的基础名:
        
        国际化配置文件的基础名是:去掉语言和国家
            login.properties

        2. 点击properties.getBasename()看一下:

        3. 发现国际化配置文件的基础名被改成了:messgaes
        
        意味着我们的配置文件应当放在类路径下叫:messages.properties
        
        这样我们就能满足自动配置类的要求,也即不用再配置其他就能用到国际化自动配置
        类的功能了。
        
        4. prefix = "spring.messages"
        我们下面就可以用spring.messages.basename来设置基础名:
        
        因为spring.messages是前缀prefix
        basename是MessageSourceProperties中的属性

        MessageSourceProperties中basename属性上的注释:
        基础名可以包含一个指定的包名,如果不包含将从类路径的根目录下寻找。
        
    5. 配置基础名
            #指定期初名
            spring.messages.basename=i18n.login
    这样我们就将原来自动配置要求的spring.messages.basename=messages改成了
    我们的login.properties。
    
    
    6. login.html页面获取国际化的值
        
        Tymeleaf中的#{} 表达式就是取国际化信息的。
        
        将页面的文本信息用th:text="#{login.xxx}替换掉。
        
        注意:input框中的信息替换:
            Input不能用th:text,因为th:text是标签体里面的内容。
            input它是一个自结束的标签,所有没有标签体<>xxx</>
            我们可以用thymeleaf的行内表达式:[[]]
            
            <input type="checkbox" value="remember-me"/> [[#{login.remember}]]
            
            placeholder="Password"也要被th:placeholder="#{}"替换

    7. 测试访问一下
    
        1. 浏览器的语言:

        2. 这里有一个很奇怪的报错,原因是我在html页面中加了注解:

        3. 测试一下国际化效果:出现乱码

            原因是login.properties中写的是中文,而IDEA没有给我们进行编码,那么
        login.properties中的中文最终都要转成ASCII码。
        
        
        4.  我们需要在IDEA中设置一下编码:
            UTF-8,并且自动转ASCII码

            我们之前在其他项目设置过了编码,却发现properties中的文件有乱码了。

            因为setting里面的设置只是对当前项目的设置,我们设置一下全局的。

            我们在将login.properties中的中文重新编写一下。

        5. 再来测试一下;

    8.  实现的效果:
        
        根据浏览器语言设置的信息切换了国际化。

    9.  改进
        
        1. 分析
        希望点击页面上的中文求切换成中文页面,点击English切换成英文页面。
        
        这根我们SpringMVC中的一个原理有关。
        
            国际化之所以有效果,是因为有一个国际化对象Locale(区域信息对象);获取区
            域信息对象时有一个组件LocaleResolver区域信息解析器(获取区域信息对象)
            
        发现WebMvcAutoConfiguraton其中配置的有LocaleResolver
        
        所以SpringBoo也默认配置了区域信息解析器:
		@Bean
		@ConditionalOnMissingBean
		@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
		public LocaleResolver localeResolver() {
			if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
				return new FixedLocaleResolver(this.mvcProperties.getLocale());
			}
			AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
			localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
			return localeResolver;
		}
        默认的区域信息解析器就是根据请求头带来的区域信息获取Locale(区域信息对象)
        进行国际化。

        2. 改进
        
        我们在链接上携带区域信息
        
        我们可以自己来写一个localeResolver,让他来根据我们点击相关按钮来进行语言切换。
    public class MyLocaleResolver implements LocaleResolver {
        @Override
        public Locale resolveLocale(HttpServletRequest httpServletRequest) {
            String l = httpServletRequest.getParameter("l");
            System.out.println(l);
    
            //如果没有指定语言参数,就用系统默认的从请求头获取区域信息来实例化区域对象
            Locale locale = Locale.getDefault();
            if(!StringUtils.isEmpty(l)){
                String[] split = l.split("-");
                //根据链接上携带的区域信息,实例化区域对象
                locale = new Locale(split[0], split[1]);
            }
            return locale;
        }
    
        @Override
        public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
    
        }
    }
        再将区域对象加到IOC中
        @Configuration
        public class MyConfig implements WebMvcConfigurer {
            //注意2.0以上不再实现WebMvcConfigurerAdapter
        
            //将我们的区域解析器加到IOC中
            @Bean
            public LocaleResolver localeResolver(){
                return new MyLocaleResolver();
            }
        }
    10 实现国际化效果。

3. 登录功能

3.1 登录验证

    简化操作,不查询数据库。用户名不为空,密码为123456即可登录
    
    1. LoginController
@Controller
public class LoginController {

    //@RequestMapping(value = "/user/login", method = RequestMethod.POST)
    @PostMapping(value = "/user/login")  //代替上面的注解
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password,
                        Map<String, Object> map){

        if(!StringUtils.isEmpty(username) && "123456".equals(password)){
            //登陆成功:来到主页
            return "dashboard";
        }else{
            //登录失败:显示错误提示
            map.put("msg", "用户名或密码错误");
            return "login";
        }

        //返回值会被模版引擎解析成:类路径下/templates.dashboard.html

    }
}
    @RequestMapping(value = "/user/login", method = RequestMethod.POST)
    @PostMapping(value = "/user/login")
    这两个注解效果一样。
      
    返回值会被模版引擎解析成:类路径下/templates.dashboard.html
    return "dashboard";

3.2 开发技巧:实现及时的更新更改的html页面

        1.模版引擎的缓存要关掉
        spring.thymeleaf.cache=false
        2.再在更改的html上:Ctrl+F9重新编译一下。
        
        实现了实时更新html页面。

3.3 前台页面显示提示信息

    判断:用Thymeleaf的工具对象strings来判断字符串是否为空,#strings引用内置的对象。
    如果msg不为空,则显示提示信息。
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>

3.4 来到成功页面

3.4.1 解决表单重复提交
    1.  注意一个现象:表单重复提交
        刷新成功页面,会提示重新提交表单:

    2.  原因
        由于我们登录时发送的是user/login,Post请求。验证成功后,转发到了成功页面dashboard.html。
        但是地址栏还是user/login
        
        所以我们刷新页面发送的还是上一次的登录请求,表单重复提交又进行了一次登录验证。
        
        防重复提交:
        重定向来到我们的成功页面。
        
    3.  LoginController
    
        登陆成功:防止表单重复提交,可以重定向到我们的主页
        return "redirect:/main.html";
    @PostMapping(value = "/user/login")  
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password,
                        Map<String, Object> map){

        if(!StringUtils.isEmpty(username) && "123456".equals(password)){
            //登陆成功:防止表单重复提交,可以重定向到我们的主页
            return "redirect:/main.html";
        }else{
            //登录失败:显示错误提示
            map.put("msg", "用户名或密码错误");
            return "login";
        }
    }
        在定义一个视图解析:
        main.html --- > dashboard.html
@Bean 
public WebMvcConfigurer webMvcConfigurer(){
    WebMvcConfigurer configurer = new WebMvcConfigurer(){
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/main.html").setViewName("dashboard");
        }
    };
    return configurer;
}
    4.  成功页面

3.4.2 拦截器进行登录检查
    1.  我可以直接访问/main.html,就会直接来到成功页面,跳过了登录验证。
    
    2.  修改LoginController:将登录的用户放到session
@Controller
public class LoginController {

    @PostMapping(value = "/user/login")  
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password,
                        Map<String, Object> map, HttpSession session){

        if(!StringUtils.isEmpty(username) && "123456".equals(password)){

            //将登录用户放到session中,拦截器获取,来进行拦截。
            session.setAttribute("loginUser", username);

            //登陆成功:防止表单重复提交,可以重定向到我们的主页
            return "redirect:/main.html";
        }else{
            //登录失败:显示错误提示
            map.put("msg", "用户名或密码错误");
            return "login";
        }
    }
}
    3.  我们用拦截器进行登录检查:获取session是否有登录用户
        编写一个拦截器:LoginHandlerInterceptor
public class LoginHandlerInterceptor implements HandlerInterceptor {

    //目标执行之前,执行该方法
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object user = request.getSession().getAttribute("loginUser");
        if(user == null){
            //未登录,返回登录页面
            request.setAttribute("msg", "没有权限请先登录");
            request.getRequestDispatcher("/index.html").forward(request, response);
            return false;
        }else{
            //已登录,放行请求
            return true;
        }

    }

    @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 {

    }
}
    4.  在自定义配置中:加载该拦截器
@Bean 
public WebMvcConfigurer webMvcConfigurer(){
    WebMvcConfigurer configurer = new WebMvcConfigurer(){
        //配置视图解析器
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/").setViewName("login");
            registry.addViewController("/index.html").setViewName("login");
            registry.addViewController("/main.html").setViewName("dashboard");
        }

        //配置拦截器
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**")
                    .excludePathPatterns("/index.html","/","/user/login");
        }

        //配置静态资源
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
        }
    };
    return configurer;
}
    注意:
        1.拦截任意多层路径下的任意请求,排除掉访问登录请求,和验证请求。
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**")
        .excludePathPatterns("/index.html","/","/user/login");
       2.注意静态资源的释放:
       静态资源:以前SpringBoot已经做好了静态资源的映射,new WebMvcConfigurerAdapter()
       时不用我们再来处理了。
       
       但是WebMvcConfigurerAdapter在SpringBoot2.0就已经被淘汰了,此时我们
       new WebMvcConfigurer()不是new WebMvcConfigurerAdapter(),所以我们
       要重写一个方法来释放静态资源
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
}
    5. 将Company name改成登录用户

    6. 测试效果

4. CRUD员工列表

4.1 Restful风格

实验要求:
    1). RestfulCRUD:CRUD要满足Rest风格--请求的URL看起来很清爽
        URL格式:/资源名称/资源标识 HTTP请求方式来区分对资源的CRUD操作
普通的CRUD(uri来区分操作) RestfulCRUD
查询 getEmp emp---GET
添加 addEmp?xxx emp---POST
修改 updateEmp?id=xx&xxx=xxx emp/{id}---PUT
删除 deleteEmp?id=1 emp/{id}---DELETE
    2). 实验的Restful架构
请求的URL 请求的方式
查询所有员工 emps GET
查询某个员工 emp/{id} GET
来到添加页面 emp GET
添加员工 emp POST
来到修改页面(查询员工进行信息回显) emp{id} GET
修改员工 emp PUT
删除员工 emp{id} DELETE

4.2 员工列表---公共元素抽取

4.2.1 抽取方法一 ~{templatename::selector}
    thymeleaf公共页面元素抽取

1. 抽取公共片段
    <div th:fragment="copy">
    &copy; 2011 The Good Thymes Virtual Grocery
    </div>
2. 引入重用片段
    <div th:insert="~{footer :: copy}"></div>
    
    ~{templatename::selector}:模板名::选择器
    ~{templatename::fragmentname}:模板名::片段名
3.  默认效果:
    insert的公共片段在div标签中
    如果使用th:insert等属性进行引入,可以不用写~{}:
    行内写法可以加上:[[~{}]];[(~{})];
4.  示例:

    主页的头部栏

    查询员工的头部栏

    所以我们要将公共代码部分抽取出来
        1.  从dashboard.html页面抽取<nav></nav>中的公共代码
            
            th:fragment="topbar"

        2. 将抽取出来的公共部分,在list.html引入
            <div th:insert="~{dashboard::topbar}"></div>

    注意:得到的效果是,抽取出的公共代码片段在引入的页面里被<div/>包了起来。
          结果是要引入这个代码段的页面和原来相比多了一个</div>。
          
          可能会在将来的对其他元素造成影响。

5.  将其真正抽取出来
    三种引入公共片段的th属性
    1.  th:insert
        将公共片段整个插入到声明引入的元素(<div/>)中
    2.  th:include
        将声明引入的元素<div/>替换为公共片段
    3.  th:replace
        将被引入的片段的内容包含进这个标签中
        <footer th:fragment="copy">
            &copy; 2011 The Good Thymes Virtual Grocery
        </footer>
        
        引入方式
        <div th:insert="footer :: copy"></div>
        <div th:replace="footer :: copy"></div>
        <div th:include="footer :: copy"></div>
        
        效果
        <div>
            <footer>
            &copy; 2011 The Good Thymes Virtual Grocery
            </footer>
        </div>
        
        <footer>
            &copy; 2011 The Good Thymes Virtual Grocery
        </footer>
        
        <div>
            &copy; 2011 The Good Thymes Virtual Grocery
        </div>
6. 所以我们这里引入公共代码片段应该用replace 

    这里就不会出现<div/>标签了
4.2.1 抽取方法二 选择器selector
    1. dashboard.html

    2. list.html

4.2.3 引入代码片段时传入参数
    需求效果:动态高亮点击选项

    1.  将可以公共音容的片段都放入commons文件夹中
    
    将公共代码抽取出来放到commons文件夹bar.html种

    2.  引入公共代码部分
    
        1. dashboard.html

        2. list.html

    3. 引入代码片段时传入参数
    
        实现动态高亮点击的选项。
        1. 在templates/commons/bar.html 中的公共代码片段定义一个参数

        2.  在引入公共代码片段的地方传入这个参数
        
        点击Dashboard会跳转到dashboard.html,其中引入了bar中的sider部分,
        并传入activerUri参数。参数满足bar.html的条件时,就能激活active样式。
        
        同理点击员工管理时,跳转到list.html时也在引入siderbar时,传入参数。
        
        这样点击谁,就能动态的实现高亮样式。

4.3 实现查询所有

    1.  EmployeeController
    //查询所有员工返回列表页面
    @GetMapping("/emps")
    public String list(Model model){

        Collection<Employee> employees = employeeDao.getAll();

        //放在请求域中
        model.addAttribute("emps", employees);

        //thymeleaf默认拼串:classpath:/templates/   +  emp/list  + .html
        return "emp/list";

    }
    2. list.html
<table class="table table-striped table-sm">
<thead>
	<tr>
		<th>#</th>
		<th>lastName</th>
		<th>email</th>
		<th>gender</th>
		<th>department</th>
		<th>birth</th>
		<th>操作</th>
	</tr>
</thead>
<tbody>
	<tr th:each="emp:${emps}">
		<td th:text="${emp.id}"></td>
		<td th>[[${emp.lastName}]]</td>
		<td th:text="${emp.email}"></td>
		<td th:text="${emp.gender}==0?'女':'男'"></td>
		<td th:text="${emp.department.departmentName}"></td>
		<td th:text="${#dates.format(emp.birth, 'yyyy-MM-dd HH:mm')}"></td>
		<td>
			<button class="btn btn-sm btn-primary">编辑</button>
			<button class="btn btn-sm btn-danger">删除</button>
		</td>
	</tr>
</tbody>
</table>

4.4 员工添加 -- 配置日期格式转换

注意:提交生日日期时,提交的数据格式不对

    2017-12-12; 2016/12/12; 2020.12.12
    
    日期格式的转换:
        SpringMVC将页面提交的值需要转换为指定的类型。
        默认的日期转换格式时:
        2016/12/12  --- Date
        
        
    
    查看WebMvcProperties中的属性配置
	public static class Format {

		/**
		 * Date format to use, for example `dd/MM/yyyy`.
		 */
		private String date;

		/**
		 * Time format to use, for example `HH:mm:ss`.
		 */
		private String time;

		/**
		 * Date-time format to use, for example `yyyy-MM-dd HH:mm:ss`.
		 */
		private String dateTime;
	
	}
    所以SpringBoot中默认的日期格式转换是:
        yyyy/MM/dd ---> Date
        yyyy-MM-dd HH:mm:ss ----> dateTime

    配置成我们需要的日期格式转换:

4.5 员工修改

    注意:此案例中员工修改也在add.html页面
    
    1.点击编辑按钮,发送带id参数的请求:

    2.在controller取出该id参数,并根据参数查询出employee

public String toEditPage(@PathVariable("id") Integer id, Model model)

        //来到修改页面,查出当前员工,在页面显示
        @GetMapping("/emp/{id}")
        public String toEditPage(@PathVariable("id") Integer id, Model model){
            Employee employee = employeeDao.get(id);
            
            model.addAttribute("emp", employee);
            
            //回到编辑页面
            return "emp/edit";
        }
    3.  在编辑页面显示该员工原本的信息。

        页面效果:

    4. 由于添加和修改操作都在add.html页面。
    
    所以此时我们再点击添加操作时,add.html也会执行我们刚才在编辑操作中加入的操作:
    显示各种属性。
    但是此时各奔就没有传过来emp对象,所以报错。

    5.  判断是添加操作还是修改操作:
        判断依据:
            添加操作:没有传过来emp对象
            修改操作:传过来emp对象

4.6 员工修改---PUT请求

    1.  表单只能提交 GET/POST请求
    
    2.  在SpringMVC中配置HiddenHttpMethodFilter()
    
        SpringBoot自动配配置的是关闭:
        spring.mvc.hiddenmethod.filter.enabled=false
        
        所以要在主配置文件中开启:
        spring.mvc.hiddenmethod.filter.enabled=true
    
    3. 页面创建一个post表单
    
    4. 在提交的表单中创建一个input项, name="_method" value="put"; 值就是我们指定的请求方式
    
    5.  写一个hidden input

        查看修改时的源码了。

        发现页面修改生效了。
        
    6.  提交员工的id保存修改操作

        在add.html页面中将id传过去。

    7.  controller中的相关方法
        //来到修改页面,查出当前员工,在页面显示
        @GetMapping("/emp/{id}")
        public String toEditPage(@PathVariable("id") Integer id, Model model){
            Employee employee = employeeDao.get(id);
            model.addAttribute("emp", employee);
    
            //页面要显示所有的部门列表
            Collection<Department> departments = departmentDao.getDepartments();
            model.addAttribute("departs",departments);
    
            //回到编辑页面(add是一个修改添加二合一的页面)
            return "emp/add";
        }
    
        //员工修改:PUT
        @PutMapping("/emp")
        public String updateEmployee(Employee employee){
    
            System.out.println("修改的员工数据:" +employee);
    
            employeeDao.save(employee);
    
            return "redirect:/emps";
        }

4.7 员工删除

    1. 删除操作是 DELETE请求
    
    2. 在提交的表单中创建一个input项, name="_method" value="put"; 值就是我们指定的请求方式

        这样会有个问题是:
            因为<form/>在<tr th:each="emp:${emps}"/>中,所以每遍历一个就会生成一个表单。
            会导致页面改变。

    3. 解决
        
        1.  自定义th属性
            th:attr="key=value, key=value"

<button th:attr="del_uri=@{/emp/}+${emp.id}" class="btn btn-sm btn-danger deleteBtn">删除</button>

<button class="btn btn-sm btn-danger deleteBtn" del_uri="/crud/emp/1005">删除</button>

        定义一个属性:del_uri="/crud/emp/id"

        2.  将deleteForm表单拿出来

        3.  为deleteBtn设置一个点击事件。
        
            当点击删除按钮时,触发事件,获取deleteForm表单,设置属性action为自定义的属性
            del_uri="/crud/emp/id",然后在提交表单。