学习流程-2024-12
学习流程2024-12-01
1.请求/或/index.htmL无法跳到登录页login.html的原因: 项目中没有引入thymeleaf。(另外注意:这里我一开始写错了,在controller上加了@EnableWebMvc注解)
@Controller public class IndexController { /** * 跳首页(这里是登录页) * * 请求/或/index.htmL无法跳到登录页login.html的原因: * 项目中没有引入thymeleaf * @return */ @RequestMapping({"/", "/index.html"}) public String goSignInPage() { return "index"; } }
学习流程2024-12-04
1.访问首页,静态资源例如样式、图片没加载出来:解决方法:需要引入thymeleaf命名空间xmlns:th="http://www.thymeleaf.org",然后将html文件里引入的静态资源改成thyemleaf的写法:例如:th:src={@/xx}、th:href={@/yy}
<!DOCTYPE html> <!-- 需要引入thymeleaf命名空间 --> <html xmlns:th="http://www.thymeleaf.org" lang="en"> <head> <!-- <link href="asserts/css/bootstrap.min.css" rel="stylesheet">--> <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"> <!-- <link href="asserts/css/signin.css" rel="stylesheet">--> <link th:href="@{/css/signin.css}" rel="stylesheet"> </head> <body> <script type="text/javascript" th:src="@{/js/jquery-3.2.1.slim.min.js}" ></script> <script type="text/javascript" th:src="@{/js/popper.min.js}" ></script> <script type="text/javascript" th:src="@{/js/bootstrap.min.js}" ></script> <script type="text/javascript" th:src="@{/js/feather.min.js}" ></script> <script type="text/javascript" th:src="@{/js/Chart.min.js}" ></script> <form class="form-signin" action="dashboard.html"> <img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72"> </form> </body> </html>
2.更通用的方法是如下定义配置类,这样无需使用IndexController了(但实际我测试时即使indexController和配置类都注释掉,请求http://localhost:8086/也能直接跳到index.html,以后再弄明白这问题,先继续往后走)
package com.kuang.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author konglingchao * @Description: * @date 2024/12/4 */ @Configuration public class MyMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("index"); registry.addViewController("/index.html").setViewName("index"); } }
学习流程2024-12-06
1.登录功能初始练习,这里还没加登录后重定向解决url里带用户名及密码的问题,下一步加上
@Controller public class LoginController { /** * 初版验证能否跳进来 * @return */ // @RequestMapping("/user/login") // @ResponseBody // public String loginFirstPractice() { // return "ok"; // } /** * 验证直接跳首页dashboard * @return */ // @RequestMapping("/user/login") // public String loginSecondPractice() { // return "dashboard"; // } /** * 验证接收参数 * @param username * @param passwrod * @return */ // @RequestMapping("/user/login") // public String loginThirdPractice(@RequestParam("username") String username, @RequestParam("password") String passwrod) { // System.out.println(username); // System.out.println(passwrod); // return "dashboard"; // } /** * 练习:用户名为1,密码为2即放行 * @param username * @param passwrod * @param model * @return */ // @RequestMapping("/user/login") // public String loginFourthPractice(@RequestParam("username") String username, // @RequestParam("password") String passwrod, // Model model) { // // 练习:用户名为2,密码为1即放行 // if ("2".equals(username) && "1".equals(passwrod)) { // return "dashboard"; // } else { // return "index"; // } // } @RequestMapping("/user/login") public String loginPractice(@RequestParam("username") String username, @RequestParam("password") String passwrod, Model model) { // 练习:用户名为2,密码为1即放行 if ("2".equals(username) && "1".equals(passwrod)) { return "dashboard"; } else { // 当密码不对时,跳登录页,同时给出提示 model.addAttribute("msg", "用户名或密码错误,请重新输入"); return "index"; } } }
前端当model中有msg消息时,将其显示在页面上
<!-- th:if="${not #strings.isEmpty(msg)}" not是取反。 #strings是thymeleaf提供的工具类 #strings.isEmpty()是thymeleaf提供的函数,判断变量是否为空--> <!-- th:text="${msg}"取后端设置到Model中的key为msg的(键值对)的值 --> <p style="color: red" th:if="${not #strings.isEmpty(msg)}" th:text="${msg}"></p>
学习流程2024-12-08
1.登录成功后重定向到首页。(为了解决问题:直接跳首页的话,浏览器地址栏如下:http://localhost:8086/user/login?username=2&password=1。带有表单提交的用户名密码,不合适)
@Controller public class LoginController { /** * 登录成功后重定向到首页 * 为了解决问题:直接跳首页的话,浏览器地址栏如下:http://localhost:8086/user/login?username=2&password=1 * 带有表单提交的用户名密码,不合适 * @param username * @param password * @param model * @param session * @return */ @RequestMapping("/user/login") public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model, HttpSession session) { if (!StringUtils.isEmpty(username) && "1".equals(password)) { // 直接跳首页的话,浏览器地址栏如下:http://localhost:8086/user/login?username=2&password=1 // 带有表单提交的用户名密码,不合适 // 所以这里进行重定向操作,先在MyMvcConfig.java里定义这映射,然后重定向到dashboard页面 // return "dashboard"; // 我一开始写的是错误的:return ":redirect/main"; // 重定向的正确写法是redirect:后加路径例如redirect:/main // 向session中存入loginUser session.setAttribute("loginUser", username); return "redirect:/main.html"; } else { model.addAttribute("msg", "用户名或密码错误请重新输入"); return "index"; } } }
配置类中配置用于重定向的/main.html
@Configuration public class MyMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("index"); registry.addViewController("/index.html").setViewName("index"); // 定义将/main.html映射到dashboard页面 registry.addViewController("/main.html").setViewName("dashboard"); } }
2.登录拦截器,避免用户能通过url访问到首页等资源
拦截器:
package com.kuang.config; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 登录拦截器,避免用户能通过url访问到首页等资源 * * @author konglingchao * @Description: * @date 2024/12/8 */ public class LoginHandlerInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { // 一开始我这里不会写了, // 因为之前是把信息设置到model里:model.setAttribute("msg","用户名或密码错误"); // 这里有request,可以直接向request里设置属性 request.setAttribute("msg", "请先登录"); // 请求转发 request.getRequestDispatcher("/index.html").forward(request, response); return false; } else { return true; } } }
配置文件里添加拦截器:
@Configuration public class MyMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("index"); registry.addViewController("/index.html").setViewName("index"); // 定义将/main.html映射到dashboard页面 registry.addViewController("/main.html").setViewName("dashboard"); } /** * 添加拦截器 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginHandlerInterceptor()) // 过滤所有请求 .addPathPatterns("/**") // 排除访问登录页请求、登录请求,排除静态资源 .excludePathPatterns("/", "/index.html", "/user/login", "/css/**", "/img/**", "/js/**"); } }
学习流程2024-12-09
1.登录后将左上角Company name改为登录人名字
<!-- <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Company name</a>--> <!-- 如下有问题:会显示:${session.loginUser} --> <!-- <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">${session.loginUser}</a>--> <!-- 如下有问题:会显示:${loginUser} --> <!-- <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">${loginUser}</a>--> <!-- 如下有问题:取不到值 --> <!-- <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${loginUser}]]</a>--> <!-- 如下是正确写法:因为loginUser是存在session里的 --> <!-- <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a>--> <!-- 或者用th:text这种 --> <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#" th:text="${session.loginUser}"></a>
学习流程2024-12-10
1.将404.html、dashboard.html、list.html里顶部导航栏和侧边栏代码抽取出来,避免相同代码在3个页面分别有一份
抽取出common.html:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" lang="en"> <!-- 顶部导航栏 --> <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="top-navigation-bar"> <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#" th:text="${session.loginUser}"></a> <!-- 省略余下具体内容 --> </nav> <!-- 侧边栏 --> <nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar"> <div class="sidebar-sticky"> <ul class="nav flex-column"> <li class="nav-item"> <a class="nav-link" th:href="@{/employee/goEmployeeManagePage}"> 员工管理 </a> </li> <!-- 省略余下具体内容 --> </ul> </div> </nav> </html>
需要引入common.html的页面,以dashboard.html为例:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" lang="en"> <body> <!-- 引入顶部导航栏 --> <div th:replace="~{common/common::top-navigation-bar}"></div> <div class="container-fluid"> <div class="row"> <!-- 引入侧边栏 --> <div th:replace="~{common/common::sidebar}"></div> <!-- 省略其余代码 --> </div> </div> </body> </html>
2.员工管理(列表)的跳转:EmployeeController如下,getAllEmployees方法暂未测试,下一步测试。同时之前DepartmentDao和EmployeeDao忘记了加@Repository注解,不加这注解的话,这俩类无法被spring接管,进而在EmployeeController里无法@Autowired注入employeeDao。
@RequestMapping("/employee") @Controller public class EmployeeController { @Autowired private EmployeeDao employeeDao; /** * 跳转员工管理页面 * @return */ @RequestMapping("/goEmployeeManagePage") public String goEmployeeManagePage() { return "user/userList"; } /** * 获取所有员工 * @return */ @RequestMapping("/getAllEmployees") public Collection getAllEmployees() { return employeeDao.getAllEmployees(); } }
DepartmentDao和EmployeeDao需要加@Repository注解
@Repository public class DepartmentDao { // 省略其余代码 } @Repository public class EmployeeDao { // 省略其余代码 }
学习流程2024-12-11
1.实现点击变色效果:只有从对应页面里点进来,这对应的侧边栏超链接才变色:给组件传递参数:
首页给侧边栏组件传参:
<div class="container-fluid"> <div class="row"> <!-- 引入侧边栏 --> <!-- 传递参数给组件 --> <div th:replace="~{common/common::sidebar(active='main.html')}"></div> </div> </div>
用户管理列表给侧边栏组件传参:
<div class="container-fluid"> <div class="row"> <!-- 引入侧边栏 --> <!-- 给组件传递参数 --> <div th:replace="~{common/common::sidebar(active='userList.html')}"></div> </div> </div>
组件内接收传参:common.html:
<li class="nav-item"> <!--一开始我的写法是错误的:th:class="${active==='dashboard'} ? nav-link active : nav-link" active=='main.html' ? 'nav-link active' : 'nav-link'应整个写到${}内--> <a th:class="${active=='main.html' ? 'nav-link active' : 'nav-link'}" th:href="@{/main.html}"> 首页 <span class="sr-only">(current)</span> </a> </li> <li class="nav-item"> <a th:class="${active=='userList.html' ? 'nav-link active' : 'nav-link'}" th:href="@{/employee/goEmployeeManagePage}"> 员工管理 </a> </li>
学习流程2024-12-12
1.列表查询:
EmployeeController:
@RequestMapping("/employee") @Controller public class EmployeeController { @Autowired private EmployeeDao employeeDao; /** * 跳转员工管理页面 * @return */ @RequestMapping("/goEmployeeManagePage") public String goEmployeeManagePage(Model model) { Collection<Employee> allEmployees = employeeDao.getAllEmployees(); model.addAttribute("employees", allEmployees); return "user/userList"; } }
userList.html:
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4"> <h2>Section title</h2> <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>--> <!-- <td>1,001</td>--> <!-- <td>Lorem</td>--> <!-- </tr>--> <!-- <tr>--> <!-- <td>1,002</td>--> <!-- <td>amet</td>--> <!-- </tr>--> <!--遍历 th:each="employee : ${employees}" --> <!--/*注意:当注释掉代码时,若html引入了模板引擎,且要注释的代码里有[[]], 则必须用模板引擎的注释(例如本行),否则会报错*/--> <!--/*直接写<td></td>之间则要加[[]]*/--> <!--/*<td>[[${employee.getLastName()}]]</td>*/--> <tr th:each="employee : ${employees}"> <td th:text="${employee.getId()}"></td> <!-- <td th:text="${employee.getLastName()}"></td> --> <td>[[${employee.getLastName()}]]</td> <td th:text="${employee.getEmail()}"></td> <td th:text="${employee.getGender()=='M' ? '男' : '女'}"></td> <td th:text="${employee.getDepartment().getDepartmentName()}"></td> <!-- thymeleaf自带工具类dates,#dates.format()日期格式化 --> <td th:text="${#dates.format(employee.getBirth(), 'yyyy-MM-dd HH:mm:ss')}"></td> <td> <button class="btn btn-sm btn-primary">编辑</button> <button class="btn btn-sm btn-danger">删除</button> </td> </tr> </tbody> </table> </div> </main>
学习流程2024-12-13、2024-12-15:
1.新增:
列表页的新增按钮:
<!-- <h2><a th:href="@{/employee/practiceRestfulGetGoEmployeeAdd}" class="btn btn-sm btn-success">新增职员</a></h2>--> <h2><a th:href="@{/employee/practiceRestfulApi}" class="btn btn-sm btn-success">新增职员</a></h2>
添加页:
<!-- https://v4.bootcss.com/docs/components/forms/ --> <!-- <form th:action="@{/employee/practiceRestfulPostDoEmployeeAdd}">--> <!-- 当验证两个restful风格的url时(practiceRestfulApi),这里必须要指明method="post", 否则会调到get请求那个practiceRestfulApi方法 --> <form th:action="@{/employee/practiceRestfulApi}" method="post"> <div class="form-group"> <label for="inputLastName">lastName</label> <input type="text" name="lastName" class="form-control" id="inputLastName"> </div> <div class="form-group"> <label for="inputEmail">email</label> <input type="email" name="email" class="form-control" id="inputEmail"> </div> <!-- form表单,单选框;需要对每个input都设置name属性,并设置value值 --> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="gender" id="inlineRadioMale" value="M"> <label class="form-check-label" for="inlineRadioMale">男</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="gender" id="inlineRadioFemale" value="F"> <label class="form-check-label" for="inlineRadioFemale">女</label> </div> <!-- form表单,下拉框(选择器);需要对整个selecter设置name属性, 然后对每一个选项设置value值 --> <div class="form-group"> <label for="formControlSelectDepartment">部门</label> <!-- <select class="form-control" id="formControlSelectDepartment">--> <!-- <option>1</option>--> <!-- <option>2</option>--> <!-- </select>--> <select class="form-control" name="department.id" id="formControlSelectDepartment"> <!-- 遍历departments,显示的值是部门的名字,往后端提交的值是department的id --> <option th:each="department : ${departments}" th:text="${department.getDepartmentName()}" th:value="${department.getId()}"></option> </select> </div> <!-- spring通常支持的是yyyy/MM/dd。 如果要支持yyyy-MM-dd则需要修改配置:spring.mvc.date-format=yyyy-MM-dd --> <div class="form-group"> <label for="inputBirth">birth</label> <input type="text" name="birth" class="form-control" id="inputBirth"> </div> <button type="submit" class="btn btn-primary">Submit</button> </form>
后端:跳转新增页接口,及保存接口:
@RequestMapping("/employee") @Controller public class EmployeeController { @Autowired private EmployeeDao employeeDao; @Autowired private DepartmentDao departmentDao; /** * 跳转员工管理页面 * @return */ @RequestMapping("/goEmployeeManagePage") public String goEmployeeManagePage(Model model) { Collection<Employee> allEmployees = employeeDao.getAllEmployees(); model.addAttribute("employees", allEmployees); return "user/userList"; } /** * 跳添加员工页面; * 为练习restful api,这里用@GetMapping("/practiceRestfulApi") * @param model * @return */ @GetMapping("/practiceRestfulApi") public String practiceRestfulGetGoEmployeeAdd(Model model) { // 把部门列表返回前端 Collection<Department> departments = departmentDao.getAllDepartments(); model.addAttribute("departments", departments); return "user/userAdd"; } // 上下两个方法进行对照 /** * 练习保存员工 * 为练习restful api,这里用@PostMapping("/practiceRestfulApi") * @param employee * @return */ @PostMapping("/practiceRestfulApi") public String practiceRestfulPostDoEmployeeAdd(Employee employee) { System.out.println(employeeDao.getAllEmployees()); employeeDao.addEmployee(employee); // 重定向到 跳转员工列表的/employee/goEmployeeManagePage请求 return "redirect:/employee/goEmployeeManagePage"; } }
学习流程2024-12-16:
1.编辑:
列表页跳修改页的链接:
<td> <a th:href="@{/employee/goEditPage/}+${employee.getId()}" class="btn btn-sm btn-primary">编辑</a> </td>
编辑页面:
<!-- https://v4.bootcss.com/docs/components/forms/ --> <form th:action="@{/employee/updateOneEmployee}" method="post"> <!-- 一定要传id,hidden,隐藏域,否则后端获取不到id --> <input th:value="${employee.getId()}" type="text" name="id" hidden="hidden"> <div class="form-group"> <label for="inputLastName">lastName</label> <input th:value="${employee.getLastName()}" type="text" name="lastName" class="form-control" id="inputLastName"> </div> <div class="form-group"> <label for="inputEmail">email</label> <input th:value="${employee.getEmail()}" type="email" name="email" class="form-control" id="inputEmail"> </div> <!-- form表单,单选框;需要对每个input都设置name属性,并设置value值 --> <div class="form-check form-check-inline"> <input th:checked="${employee.getGender() == 'M'}" class="form-check-input" type="radio" name="gender" id="inlineRadioMale" value="M"> <label class="form-check-label" for="inlineRadioMale">男</label> </div> <div class="form-check form-check-inline"> <input th:checked="${employee.getGender() == 'F'}" class="form-check-input" type="radio" name="gender" id="inlineRadioFemale" value="F"> <label class="form-check-label" for="inlineRadioFemale">女</label> </div> <!-- form表单,下拉框(选择器);需要对整个selecter设置name属性, 然后对每一个选项设置value值 --> <div class="form-group"> <label for="formControlSelectDepartment">部门</label> <!-- <select class="form-control" id="formControlSelectDepartment">--> <!-- <option>1</option>--> <!-- <option>2</option>--> <!-- </select>--> <select class="form-control" name="department.id" id="formControlSelectDepartment"> <!-- 遍历departments,显示的值是部门的名字,往后端提交的值是department的id --> <option th:selected="${department.getId() == employee.getDepartment().getId()}" th:each="department : ${departments}" th:text="${department.getDepartmentName()}" th:value="${department.getId()}"></option> </select> </div> <!-- spring通常支持的是yyyy/MM/dd。 如果要支持yyyy-MM-dd则需要修改配置:spring.mvc.date-format=yyyy-MM-dd --> <div class="form-group"> <label for="inputBirth">birth</label> <!-- 默认展示类似:Mon Dec 16 19:55:09 CST 2024,需用#dates.format()来转换, 同时格式需要与spring默认配置一致,或与application.properties里配置一致。 否则提交时无法识别会报错。--> <input th:value="${#dates.format(employee.getBirth(), 'yyyy-MM-dd')}" type="text" name="birth" class="form-control" id="inputBirth"> </div> <button type="submit" class="btn btn-primary">Submit</button> </form>
后端跳转及保存接口:
@RequestMapping("/employee") @Controller public class EmployeeController { @Autowired private EmployeeDao employeeDao; @Autowired private DepartmentDao departmentDao; /** * 跳转员工管理页面 * @return */ @RequestMapping("/goEmployeeManagePage") public String goEmployeeManagePage(Model model) { Collection<Employee> allEmployees = employeeDao.getAllEmployees(); model.addAttribute("employees", allEmployees); return "user/userList"; } /** * 跳编辑页 * @param employeeId * @param model * @return */ @GetMapping("/goEditPage/{employeeId}") public String goEditPage(@PathVariable("employeeId") int employeeId, Model model) { // 要传雇员信息 Employee employeeById = employeeDao.getEmployeeById(employeeId); // 要传部门信息 Collection<Department> departments = departmentDao.getAllDepartments(); model.addAttribute("employee", employeeById); model.addAttribute("departments", departments); return "user/userEdit"; } /** * 更新员工 * @param employee * @return */ @PostMapping("/updateOneEmployee") public String updateOneEmployee(Employee employee) { employeeDao.updateEmployee(employee); return "redirect:/employee/goEmployeeManagePage"; } }
学习流程2024-12-17:
1.删除:
前端页面:
<td> <a th:href="@{/employee/deleteById/}+${employee.getId()}" class="btn btn-sm btn-danger">删除</a> </td>
后端:
@RequestMapping("/employee") @Controller public class EmployeeController { @Autowired private EmployeeDao employeeDao; @Autowired private DepartmentDao departmentDao; /** * 跳转员工管理页面 * @return */ @RequestMapping("/goEmployeeManagePage") public String goEmployeeManagePage(Model model) { Collection<Employee> allEmployees = employeeDao.getAllEmployees(); model.addAttribute("employees", allEmployees); return "user/userList"; } /** * 删除员工 * @param employeeId * @return */ @GetMapping("/deleteById/{employeeId}") public String deleteById(@PathVariable("employeeId") int employeeId) { employeeDao.deleteEmployee(employeeId); return "redirect:/employee/goEmployeeManagePage"; } }
2.请求报404时跳404.html:
<!-- 在templates下新建error文件夹,然后把404.html放到error文件夹下,这样当404时就会自动跳转到404.thml页面 -->
3.退出登录:
前端:
<a class="nav-link" th:href="@{/user/logout}">退出登录</a>
后端:
@Controller public class LoginController { /** * 退出登录 * @param session * @return */ @RequestMapping("/user/logout") public String logout(HttpSession session) { // 使当前会话失效,清空会话中所有的属性。 session.invalidate(); return "redirect:/index.html"; } }
4.类似Tomcat的项目虚拟目录:
application.properties:
# 类似于Tomcat的项目虚拟目录 # 配置后对网页右击”查看网页源代码“, # 就能看到资源类似于<link href="/kuang/css/bootstrap.min.css" rel="stylesheet">, # 自动加上了/kuang server.servlet.context-path=/kuang
项目里表单的提交相关:
<!-- 表单提交也要改成th:action="@{}"这种,否则设置server.servlet.context-path后会失效 --> <form class="form-signin" th:action="@{/user/login}">