Spring Boot |使用SpringBoot進行Restful-CRUD


本文介紹使用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";
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章