文章目錄
本文介紹使用SpringBoot進行一個Restful風格的CRUD,項目全部代碼已經上傳,可在文章附錄中下載進行練習,下面對CRUD的主要功能做分析。
注意:提供的
靜態頁面(html)放在resources文件夾下不會得到模板引擎的解析
,正確的做法是放在templates文件夾下;而靜態資源(css,img,js)放入static文件夾即可。
項目的配置類application.properties
# 映射
server.servlet.context-path=/crud
#指定生日日期格式
spring.mvc.date-format=yyyy-MM-dd
# 禁用緩存
spring.thymeleaf.cache=false
# 設置區域信息解析器
spring.messages.basename=i18n.login
# 支持delete
spring.mvc.hiddenmethod.filter.enabled=true
一、實現WebMvcConfigurer擴展SpringMVC的功能
1.1 添加視圖映射
重寫addViewControllers
方法。 將訪問路徑url中的/
和/index.html
都映射爲login
,將訪問路徑url中的/main.html
映射爲dashboard
。
//視圖映射
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");//登錄頁面
registry.addViewController("/index.html").setViewName("login");//主頁
registry.addViewController("/main.html").setViewName("dashboard");//主頁
}
類似的:配置文application.properties
件中的server.servlet.context-path=/crud
相當於將localhost:8080
映射爲了localhost:8080/crud
。
1.2 添加攔截器
- addPathPatterns:設置需要攔截的請求。
- excludePathPatterns:設置排除攔截的請求。
//添加攔截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/index.html","/","/user/login","/static/**","/webjars/**");
}
1.3 註冊自定義區域信息解析器
爲了讓我們的自定義區域信息解析器生效,除了進行國際化組件的添加外,還需要將其添加到容器中。
//註冊解析器區域信息
@Bean
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}
1.4完整的擴展代碼
@Configuration
class MyMVCConfig implements WebMvcConfigurer {
/**
* 添加攔截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/index.html","/","/user/login","/static/**",
"/webjars/**");
}
/**
* 添加視圖映射
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
registry.addViewController("/index.html").setViewName("login");
registry.addViewController("/main.html").setViewName("dashboard");
}
//註冊自定義區域解析器
@Bean
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}
}
二、添加國際化組件
之前使用SpringMVC進行國際化的步驟是:
- ①編寫國際化配置文件
- ②使用
ResourceBundleMessageSource
管理國際化資源文件 - ③在頁面使用fmt:message取出國際化內容
2.1 編寫國際化配置文件
SpringBoot自動配置好了許多組件,只需要編寫國際化配置文件,下面創建國際化文件夾
可以通過視圖界面進行國際化配置。
2.2 自動管理
spring.messages.basename=i18n.login
SpringBoot已經自動配置好了管理國際化資源文件的ResourceBundleMessageSource組件,可以通過spring.messages.messages
設置配置文件的基礎名,本項目設置爲spring.messages.basename=i18n.login
。這樣SpringBoot就將配置文件管理了起來。
源碼:
public class MessageSourceAutoConfiguration {
...
@Bean
@ConfigurationProperties(prefix = "spring.messages")//spring.messages.messages設置配置文件的基礎名
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
//public class MessageSourceProperties {
// ...
// private String basename = "messages";
// ...
//}
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();//這裏已經配置好了!!!
if (StringUtils.hasText(properties.getBasename())) {
//設置國際化資源文件的基礎名(去掉語言國家代碼的,如本項目中爲login)
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;
}
...
}
2.3 使用thymeleaf模板引擎在頁面取值
模板引擎thymeleaf的國際化使用
- 標籤體中:
#{th:text="${msg}}
- 行內表達式:
[[#{msg}]]
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Restful-CRUD</title>
<!-- Bootstrap core CSS -->
<link href="asserts/css/bootstrap.min.css" th:href="@{/webjars/bootstrap/4.4.1-1/css/bootstrap.css}" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="asserts/css/signin.css" th:href="@{/asserts/css/signin.css}" rel="stylesheet">
</head>
<body class="text-center">
<form class="form-signin" action="dashboard.html" th:action="@{/user/login}" method="post">
<img class="mb-4" th:src="@{/asserts/img/bootstrap-solid.svg}" src="asserts/img/bootstrap-solid.svg" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
<!--判斷條件成立,p標籤生效-->
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"> </p>
<label class="sr-only" th:text="#{login.username}">Username</label>
<input type="text" name="username" class="form-control" placeholder="Username" th:placeholder="#{login.username}" required="" autofocus="">
<label class="sr-only" th:text="#{login.password}">Password</label>
<input type="password" name="password" class="form-control" placeholder="Password" th:placeholder="#{login.password}" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"/>[[#{login.remember}]]
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
<p class="mt-5 mb-3 text-muted">© 2020-2021</p>
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
</form>
</body>
</html>
3.3 點擊鏈接切換語言
SpringBoot默認配置了區域信息解析器
,該解析器是根據請求頭帶來的區域信息獲取Locale進行國際化。
AcceptHeaderLocaleResolver
:根據請求頭帶來的區域信息獲取Locale進行國際化。
源碼:
public class WebMvcAutoConfiguration {
...
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
//如果沒有固定區域信息解析器的,就使用AcceptHeaderLocaleResolver
if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
}
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}
...
}
實現點擊鏈接切換語言,需要在鏈接上攜帶區域信息:
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
我們可以自己寫一個區域信息解析器
,實現點擊鏈接切換國際化。
- 這裏獲取的參數
l
是鏈接上攜帶的區域信息。 - 可以用Spring Framework的
StringUtils
工具檢查帶來的數據是否爲空。
public class MyLocaleResolver implements LocaleResolver {
/**
* 解析區域信息
*/
@Override
public Locale resolveLocale(HttpServletRequest request) {
String l = request.getParameter("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 request, HttpServletResponse response, Locale locale) {
}
}
接下來將我們的區域信息解析器添加到容器
就可以使用了。
三、登陸功能與登錄檢查
3.1 登錄功能
將longin頁面的提交地址改爲/user/login
,並且是post方式的請求。
<form class="form-signin" action="dashboard.html" th:action="@{/user/login}" method="post">
接收請求的處理器:
- 這裏只做簡單的登錄功能,當用戶名不爲空且密碼爲123456即可登錄成功。
- 登錄成功返回主頁,登錄失敗返回登錄頁面。
- 爲了防止表單重複提交,使用
redirect
重定向到主頁。 - 如果登錄成功,就將用戶的信息存入
session
中。
<!--設置p標籤:如果msg不爲空,p標籤生效,給出錯誤提示-->
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"> </p>
@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.setAttribute("loginUser",username);
//登錄成功,防止表單重複提交,重定向到主頁
return "redirect:/main.html";
} else {
map.put("msg","登錄失敗,請重新登錄~");
return "login";
}
}
}
此時,輸入正確的賬號和密碼就可跳轉到主頁了,但是存在一個安全問題,即訪問http://localhost:8080/crud/main.html
直接就進入了登錄頁面,爲此,需要添加攔截器進行登錄檢查。
3.2 使用攔截器進行登錄檢查
- 沒有登錄的用戶不能訪問後臺主頁和對員工進行增刪改查。
- request的作用域爲當前請求,所以用forward.
/**
* 攔截器:登錄檢查
*/
public class LoginHandlerInterceptor implements HandlerInterceptor {
/**
* 預檢查
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//獲得session中的登錄用戶信息
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 {
}
}
創建好了攔截器,還需要在擴展配置頁面WebMvcConfigurer
,對攔截器進行添加。
四、Restful-CRUD開始
實驗要求
Restful風格的CRUD | 普通CRUD | Restful-CRUD |
---|---|---|
查詢 | getEmp | emp----(GET方式) |
添加 | addEmp?xxx | emp----(POST方式) |
修改 | updateEmp?id=xxx&xxx | emp/{id}----(PUT方式) |
刪除 | deleteEmp?id=1 | emp/{id}----(DELETE方式) |
本實驗的請求架構 | 請求URI | 請求方式 |
---|---|---|
1.查詢所有員工 |
emps | GET |
查詢單個員工(即來到修改頁面的操作) | emp/{id} | GET |
2.來到添加頁面 |
emp | GET |
3.添加員工 |
emp | POST |
4.來到修改頁面(查出員工信息並回顯) |
emp/{id} | GET |
5.修改員工 |
emp | PUT |
6.刪除員工 |
emp/{id} | DELETE |
4.1 點擊按鈕來到list頁面
①查詢所有員工
點擊員工管理按鈕,發送GET方式的/emps
請求。
<a th:href="@{/emps}">員工管理</a>
Controller處理器接收請求後,查詢所有員工
並返回到列表頁面。
@GetMapping("/emps")
public String list(Model model) {
Collection<Employee> employees = employeeDao.getAll();
//放在請求域中進行共享
model.addAttribute("emps",employees);
return "emp/list";
}
遍歷查詢所有員工的操作:
- 使用
#dates.format(emp.birth, 'yyyy-MM-dd')
格式化日期。
list.html
<tbody>
<tr th:each="emp:${emps}">
<td th:text="${emp.id}"></td>
<td>[[${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')}"></td>
<td>
<a class="btn btn-sm btn-primary" th:href="@{/emp/}+${emp.id}">編輯</a>
<button th:attr="del_uri=@{/emp/}+${emp.id}" class="btn btn-sm btn-danger deleteBtn">刪除</button>
</td>
</tr>
</tbody>
4.2 抽取公共頁面
由於員工列表頁面、主頁面的上邊欄
和側邊欄(左)
都是一樣的,因此可以使用thymeleaf
將它們公共抽取出來。創建commons文件夾將公共代碼抽取至bar.html
頁面。
抽取公共頁面
th:fragment
:抽取名爲topbar和sidebar的兩個片段
commons/bar.html
<!--上邊欄topbar-->
<div th:fragment="topbar">
... //這裏是上邊欄的代碼
</div>
<!--側邊欄(左)sidebar-->
<div th:fragment="sidebar">
... //這裏是側邊欄(左)的代碼
</div>
引用公共片段
方式1:th:insert:將公共片段整個插入到聲明引入的元素中
方式2:th:replace:將聲明引入的元素替換爲公共片段
方式3:th:include:將被引入的片段的內容包含進這個標籤中
commons/bar::topbar
:模板名::
選擇器commons/bar::#sidebar(activeUri='main.html')
:模板名::
片段名
dashboard.html
<!--引入上邊欄topbar-->
<div th:replace="commons/bar::topbar"></div>
<!--引入側邊欄(左)sidebar-->
<div th:replace="commons/bar::#sidebar(activeUri='main.html')"></div>
list.html
<!--引入上邊欄topbar-->
<div th:replace="commons/bar::topbar"></div>
<!--引入側邊欄(左)sidebar-->
<div th:replace="commons/bar::#sidebar(activeUri='emps')"></div>
4.3 點擊按鈕高亮顯示
點擊Dashboard
或者員工管理
按鈕分別發送/main.html
、/emps
請求。使用th:class
改變獲取class的值,取出uri命名爲 activeUri
,如果activeUri==對應的請求
就生成加了active(高亮)
的標籤,否則不加active
。
dashboard.html
<a class="nav-link active" th:class="${activeUri=='main.html'?'nav-link active':'nav-link'}"
href="#" th:href="@{/main.html}">Dashboard</a>
<a class="nav-link" th:class="${activeUri=='emps'?'nav-link active':'nav-link'}"
href="#" th:href="@{/emps}">員工管理</a>
4.4 點擊添加來到添加頁面
超鏈接本身就是GET方式的請求。
list.html
<a class="btn btn-sm btn-success" href="emp" th:href="@{/emp}">員工添加</a>
Controller中對get
形式的/emp請求
進行處理。
- 返回到添加頁面前,查出所有的部門存入
depts
,方便在頁面顯示部門。
/**
* 2.來到添加頁面 /emp---GET
*/
@GetMapping("/emp")
public String toAddPage(Model model){
//來到添加頁面前,查出所有的部門,在頁面顯示
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("depts",departments);
return "emp/add";
}
th:each
:遍歷th:text
:顯示的內容th:value
:提交的value值(提交的是部門的id)th:selected
:默認選擇的部門名(這裏設置僅修改頁面有效)。
add.html
<select class="form-control" name="department.id">
<option th:selected="${emp!=null}?${dept.id==emp.department.id}" th:value="${dept.id}"
th:each="dept:${depts}" th:text="${dept.departmentName}"></option>
</select>
4.5 員工添加完成
修改員工添加form表單的action地址爲POST形式的/emp
。
add.html
<form th:action="@{/emp}" method="post">
Controller中對post
形式的/emp請求
進行處理。
- 由於thymeleaf會對返回值進行解析,進而拼串;所以要返回到list頁面,需要使用
重定向
或轉發
。 - SpringMVC自動將請求參數和入參對象的屬性進行一一綁定,需要
請求參數名
和JavaBean入參的屬性名
相同。
在add.html中將Employee屬性都加上name標籤,值爲Employee屬性的值。 - 調用employeeDao的save方法將員工數據保存。
/**
* 3.添加員工 /emp---POST
*
*/
@PostMapping("/emp")
public String addEmp(Employee employee){
//來到員工列表頁面
System.out.println("保存的員工信息:"+employee);
//保存員工
employeeDao.save(employee);
//redirect:重定向到一個地址,"/"代表當前項目路徑
//forward:轉發到一個地址
return "redirect:/emps";
}
4.5 點擊修改來到修改頁面(頁面重用)
點擊編輯,來到修改頁面。
list.html
<td>
<a class="btn btn-sm btn-primary" th:href="@{/emp/}+${emp.id}">編輯</a>
<button th:attr="del_uri=@{/emp/}+${emp.id}" class="btn btn-sm btn-danger deleteBtn">刪除</button>
</td>
Controller中對get
形式的/emp{id}
請求進行處理。
- 返回到add頁面,重用add頁面。
- 查出部門信息並保存到
depts
中,方便在頁面顯示部門。 - 查出員工id信息放在
emp
中,在表單上使用th:value="${emp.屬性名}"
回顯。
/**
* 4.來到修改頁面 /emp/{id}---GET
* 查出當前員工,在頁面回顯
*/
@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("depts",departments);
//回到修改頁面(複用add頁面)
return "/emp/add";
}
4.6 修改完成
提交時需要區分是添加還行修改頁面。因爲在修改時在model中保存了emp對象
,而添加時只有部門信息沒有員工對象。所以取值時可以據此做判斷。
${emp!=null}?${emp.屬性名}
:僅修改頁面纔回顯屬性信息。- 表單需要區分是添加請求POST還是修改請求
PUT
。 - 默認是添加頁面使用post請求,如果帶來emp有值則改爲put形式的修改頁面。
- SpringBoot已經配置好了SpringMVC的
HiddenHttpMethodFilter
,只需在form表單中將input選項項的name標籤設置爲_method
,並制定value值即可更改請求方式。 - 如果是修改頁面,需要判斷是否傳入
emp.id
的值。
add.html
<form th:action="@{/emp}" method="post">
<input type="hidden" name="_method" value="put" th:if="${emp!=null}"/>
<input th:type="hidden" name="id" th:if="${emp!=null}" th:value="${emp.id}">
以lastName屬性爲例:
<input name="lastName" type="text" class="form-control" placeholder="zhangsan" th:value="${emp!=null}?${emp.lastName}">
<button type="submit" class="btn btn-primary" th:text="${emp!=null}?'修改':'添加'"/>
Controller對put
形式的/emp
請求做處理。
- 調用employeeDao的save方法,進行修改。
/**
* 5.修改員工 /emp---PUT
* 需要提交員工id
*/
@PutMapping("emp")
public String updateEmployee(Employee employee){
System.out.println("修改員工的數據"+employee);
employeeDao.save(employee);
return "redirect:/emps";
}
4.7 刪除完成
使用js的形式提交表單。
th:attr="del_uri=@{/emp/}+${emp.id}"
:自定義使用del_uri
代表刪除請求。$(this).attr("del_uri"))
:當前按鈕的del_uri
屬性。$("#deleteEmpForm").attr("action",$(this).attr("del_uri")).submit();
:爲表單添加提交地址action
。
list.html
<button th:attr="del_uri=@{/emp/}+${emp.id}" class="btn btn-sm btn-danger deleteBtn">刪除</button>
...
<form id="deleteEmpForm" method="post" style="display:inline">
<input type="hidden" name="_method" value="delete"/>
</form>
...
<script>
$(".deleteBtn").click(function(){
//刪除當前員工
$("#deleteEmpForm").attr("action",$(this).attr("del_uri")).submit();
return false;
});
</script>
Controller中對delete
形式的/emp/{id}請求
做處理。
- 防止thymeleaf對返回值拼串,仍使用重定向。
/**
* 6.員工刪除 /emp/{id}---DELETE
*/
@DeleteMapping("/emp/{id}")
public String deleteEmployee(@PathVariable("id") Integer id){
employeeDao.delete(id);
return "redirect:/emps";
}