1. 程式人生 > >Spring Boot2 精髓--Web開發(二)

Spring Boot2 精髓--Web開發(二)

註解@PathVariable用於從請求URL中獲取引數並對映到方法引數中,如下程式碼:

@Controller
@RequestMapping("/user/{id}")
public class HelloworldController {

	@Autowired
	UserService userService;
	
	@GetMapping(path="/{type}/get.json")
	public @ResponseBody User getUser(@PathVariable Long id,@PathVariable Integer type){
		return userService.getUserById(id);
	}
}

    符號{}中的變數名與方法中的引數名一一對應,如果不想對應,如路徑中的名字是id,方法簽名是userId,則可以使用@PathVariable(“id”)long userId來對應。

    通常情況下,Java的編譯程式碼的時候,會將引數名稱也編譯到類位元組碼裡,因此春季會根據名字匹配自動對映如果上述例子中出現如下錯誤:

There was an unexpected error (type = Internal Server Error, status = 500 ).

    Name for argument type [java.lang.Long] not available,  and parameter name information not found in class file either. 

    則表明你的編譯環境未將除錯資訊加入類中,建議將編譯器改成預設設定。以Eclipse為例,選擇工程,右鍵選擇屬性,找到Java編譯器,在類檔案生成選項中勾選除內聯finally塊外的所有選項,如圖所示:

    Spring 也支援URL 中的矩陣變數,所謂矩陣變數,就是出現在URL 片段中,通過“;” 分割的多個變數,比如/user/id=123;status=1/update.json 。

JavaBean的接受HTTP 引數

    HTTP 提交的引數可以對映到方法引數上,按照名稱來對映,比如一個請求/javaBean/update.json?name=abc&id=1 ,將會對映到如下方法:

@GetMapping(path="/update.json")
public @ResponseBody User getUser(Integer id,String type){
	return userService.getUserById(id);
}

    可以通過@RequestParam 來進一步限定HTTP 引數到控制器方法的對映關係,RequestParam 支援如下屬性:

    value :指明HTTP 引數的名稱。

    需要:布林型別,宣告此引數是否必須有,如果HTTP 引數裡沒有,則會丟擲400 錯誤。

    defaultValue :字元型別,如果HTTP 引數沒有提供,可以指定一個預設字串,Spring 型別轉換為目標型別,如上一個例子,我們可以提供預設引數:

@GetMapping(path="/update.json")
public @ResponseBody User getUser(@RequestParam(value = "id", required = true, defaultValue = "12") Integer id,String type) {
	return userService.getUserById(id);
}

    可以將HTTP 引數轉為JavaBean 物件,HTTP 引數的名字對應到POJO 的屬性名。通常,HTTP 提交了多個引數,Spring 支援按照字首自動對映到不同的物件上。簡單來說,Spring 有如下表所示的HTTP 引數到JavaBean中的對映規則:

示例

解釋

名稱

物件的名稱屬性

order.name

物件的順序屬性的名稱屬性

細節[0]。名稱

物件的詳細資訊屬性,要求詳情是個陣列或者List (不能是Set ,因為Set 不具備根據索引取值的功能),details [0] 表示詳細資訊屬性的第一個元素

@RequestBody 接受JSON

    控制器方法帶有@RequestBody 註解的引數,意味著請求的HTTP 訊息體的內容是一個JSON ,需要轉化為註解指定的引數型別.Spring Boot 預設使用Jackson 來處理反序列化工作。

MultipartFile

    通過MultipartFile 來處理檔案上傳,MultipartFile 提供了以下方法來獲取上傳的檔案資訊:

    getOriginalFilename :獲取上傳的檔名字。

    getBytes :獲取上傳檔案內容,轉為位元組陣列。

    getInputStream :獲取一個InputStream 。

    isEmpty :檔案上傳為空,或者就沒有檔案上傳。

    getSize :檔案上傳的大小。

    transferTo(File dest):儲存上傳檔案到目標檔案系統。

    如果同時上傳多個檔案,則使用MultipartFile 陣列類來接受多個檔案上傳。

Spring Boot 中可以通過配置檔案application.properties 對上傳檔案進行限定,預設為如下配置:

spring.servlet.multipart.enabled=true
spring.servlet.multipart.file-size-threshold=0
spring.servlet.multipart.location=
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB
spring.servlet.multipart.resolve-lazily=false

    引數enabled 預設為true ,即允許上傳,file-size-threshold 限定了當上傳的檔案超過一定長度時,就寫到臨時檔案裡。者有助於上傳檔案不佔用過多的記憶體,單位時MB 或者KB ,預設是0 ,即不限定閥值。位置指的是臨時檔案的存放目錄,如果不設定,則網路伺服器提供一個臨時目錄。

最大檔案大小屬性指定了單個檔案的最大長度,預設是1MB ,最大請求大小的屬性說明單次HTTP 請求上傳的最大長度,預設是10MB 。解決,懶洋洋地表示當檔案和引數被訪問的時候再解析成檔案。

    如果上傳大檔案失敗,則需要檢查是不是因為Spring Boot 對檔案的限定過小造成的。另一方面,有些Spring Boot 應用設定了代理伺服器,比如設定了Apache ,也需要檢查代理伺服器是否支援大檔案上傳,是否對超時做了設定。

@ModelAttribute

    註解的ModelAttribute 通常作用做控制器的某個方法上,此方法會首先被呼叫,並將方法結果作為模型的屬性,然後再呼叫對應的控制器處理方法。

@ModelAttribute
public void findUserById(@PathVariable Long id,Model model){
	model.addAttribute("user", userService.getUserById(id));
}
	
@GetMapping(path="/{id}/get.json")
public @ResponseBody String getUser(Model model) {
	System.out.println(model.containsAttribute("user"));
	return "success";
}

    對於HTTP 請求,modelattribute / 1 / get.json ,會先呼叫findUserById 方法取得user ,並新增到模型裡。使用ModelAttribute 通常可以用來向一個Controller 中需要的公共模型新增資料。

    如果findUserById 僅僅新增一個物件到模型中,則可以改寫成如下形式:

@ModelAttribute
public void findUserById(@PathVariable Long id){
	userService.getUserById(id);
}

    這樣,返回到物件自動新增到Model 中,相當於呼叫model.addAttribute(user)。

@InitBinder

    將HTTP 引數繫結到JavaBean的物件中,其實春天框架是通過WebDataBinder 類實現這種繫結到,所以,可以在控制器中用註解@InitBinder 宣告一個方法,來自己擴充套件繫結到特性,比如:

@InitBinder
public void findUserById(WebDataBinder binder){
	binder.addCustomFormatter(new DateFormatter("yyyy-mm-dd"));
}
	
@RequestMapping(path="/date")
public @ResponseBody void printDate(Date d) {
	System.out.println(d);
	return;
}

    當需要繫結到一個Date 型別的時候,如上述程式碼所示,則採用“yyyy-MM-dd” 格式,比如使用者訪問databind / date?d = 2001-1-1 。

驗證框架

    Spring Boot 支援JSR-303 ,Bean 驗證框架,預設實現用的是Hibernate驗證器。在Spring MVC 中,只需要使用@Valid 註解標註中方法引數上,Spring Boot 即可對引數物件進行校驗,校驗結果放在BindingResult 物件中。

    JSR-303 定義了一系列註解用來驗證的Bean 的屬性,常用的有如下幾種:

@空值

驗證物件是否為空

@NotNull

驗證物件不為空

@NotBlank

驗證字串不為空或者不為空字串,比如“” “” 都會驗證失敗

@不是空的

驗證物件不為null ,或者集合不為空

@Size(MIN =,最大=)

驗證物件長度,可支援字串,集合

@長度

字串大小

@Min

驗證數字是否大於等於指定的值

@Max

驗證數字是否小於等於指定的值

@digits

驗證數字是否符合指定格式,如@digits(整數= 9,分數= 2)

@範圍

驗證數字是否在指定的範圍內,如@range(分鐘= 1,最大值為100)

@電子郵件

驗證是否為郵件格式,為則不做校驗

@圖案

驗證字串物件是否符合正則表示式的規則

    通常,不同的業務邏輯會有不同的驗證邏輯,比如對於WorkInfoForm 來說,當更新的時候,id 必須不為null ,但增加的時候,id 必須是null 。

    JSR-303 定義了group 概念,每個校驗註解都必須支援。校驗註解作用在欄位上的時候,可以指定一個或多個組,當Spring Boot 校驗物件的時候,也可以指定校驗的上下文屬於哪個組。這樣,只有組匹配的時候,校驗註解才能生效。上面的WorkInfoForm 定義id 欄位校驗可以更改為如下內容:

public class WorkInfoForm{
	//定義一個類,更新時校驗組
	public interface Update{}
	//定義一個類,新增時校驗組
	public interface Add{}
		
	@NotNull(groups={Update.class})
	@Null(groups={Add.class})
	Long id;
}

    這段程式碼表示,當校驗上下文為Add.class 的時候,@null 生效,ID 需要為空才能校驗通過;當校驗上下文為Update.class 的時候,@NotNull 生效,ID 不能為空。

MVC 中使用@Validated

    在控制器中,只需要給方法引數加上@Validated 即可觸發一次校驗。

@RequestMapping("/addworkinfo.html")
public void addWorkInfo(@Validated({WorkInfoForm.Add.class}) WorkInfoForm woekInfo,BindingResult result) {
	if(result.hasErrors()){
		List<ObjectError> list = result.getAllErrors();
		FieldError error = (FieldError) list.get(0);
		System.out.println(error.getObjectName()+","+error.getField()+","+error.getDefaultMessage());
		return;
	}
	return;
}

    此方法可以接受HTTP 引數並對映到WorkInfoForm 物件,此引數使用了@Validated 註解,將觸發Spring 到校驗,並將驗證結果存放到BindingResult 物件中。這裡,驗證註釋使用了校驗的上下文WorkInfoForm.Add .class ,因此,整個校驗將按照Add.class 來校驗。

    BindingResult 包含了驗證結果,提供瞭如下方法:

    hasErrors ,判斷驗證是否通過。

    getAllErrors ,得到所有的錯誤資訊,通常返回的是FieldError 列表。

    如果控制器引數未提供BindingResult 物件,則Spring MVC 將丟擲異常。

自定義校驗

    JSR-303 提供的大部分校驗註解已經夠用,也允許定製校驗註解,比如在WorkInfoForm 類中,我們新增一個加班時間:

@WorkOverTime
int workTime;

    屬性workTime 使用了註解@WorkOverTime ,當屬性值超過max 值的時候,將會驗證失敗.WorkOverTime 跟其他註解差不多,但提供了@Constraint 來說明用什麼類作為驗證註解實現類,程式碼如下:

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

@Constraint(validatedBy = {WorkOverTimeValidator.class})
@Documented
@Target({ElementType.ANNOTATION_TYPE,ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface WorkOverTime {
	String message()  default "加班時間過長,不能超過{max}小時";
	int max() default 5;
	Class<?> [] groups() default {};
	Class<? extends Payload>[] payload() default {};
}

    @Constraint 註解說明用什麼類來實現驗證,我們將建立一個WorkOverTimeValidator 。來進行驗證註解必須提供如下資訊:

    message ,用於建立錯誤資訊,支援表示式,如“ 錯誤,不能超過(max )小時” 。

    groups ,驗證規則分組,比如新增和修改的驗證規則不一樣,分為兩個組,驗證註解必須提供。

    有效載荷,定義了驗證的有效負荷。

    WorkOverTimeValidator 必須實現ConstraintValidator 介面初始化方法及驗證方法isValid :

package com.scg.springboot;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class WorkOverTimeValidator implements ConstraintValidator<WorkOverTime, Integer>{
	WorkOverTime work;
	int max;
	public void initialize(WorkOverTime work){
		//獲取註解的定義
		this.work = work;
		max = work.max();
	}
	@Override
	public boolean isValid(Integer value, ConstraintValidatorContext context) {
		if(value==null){
			return true;
		}
		return value<max;
	}
}

WebMvcConfigurer

    WebMvcConfigurer 是用來全域性定製化Spring Boot 的MVC 特性。開發者可以通過實現WebMvcConfigurer 介面來配置應用的MVC 全域性特性。

package com.scg.springboot;

import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfigurer implements WebMvcConfigurer{

	//攔截器
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		WebMvcConfigurer.super.addInterceptors(registry);
	}
	//跨域訪問配置
	@Override
	public void addCorsMappings(CorsRegistry registry) {
		//允許所有跨域訪問
		registry.addMapping("/**");
		//允許來自domain2.com的跨域訪問,並且限定訪問路徑為/api、方法是POST或者GET。
		registry.addMapping("/api/**")
			.allowedOrigins("http://domain2.com")
			.allowedMethods("POST","GET");
	}
	//格式化
	@Override
	public void addFormatters(FormatterRegistry registry) {
		/**
		 * 將HTTP請求對映到Controller方法的引數上後,Spring會自動進行型別轉化。對於日期型別的引數,
		 * Spring預設並沒有配置如何將字串轉為日期型別。為了支援可按照指定格式轉為日期型別,需要新增
		 * 一個DateFormatter類:
		 */
		registry.addFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss"));
	}
	//URI到檢視到對映
	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		WebMvcConfigurer.super.addViewControllers(registry);
	}
}

使用Jackson

    在MVC 框架中,Spring Boot 內建了Jackson 來完成JSON 的序列化和反序列化。在Controller 中,方法註解為@ResponseBody ,自動將方法返回的物件序列化成JSON 。如果想自己全域性自定義一個ObjectMapper 來代替預設的,則可以使用Java Config ,聯合使用@Bean ,程式碼如下:

package com.scg.springboot.controller;

import java.text.SimpleDateFormat;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.databind.ObjectMapper;

@Configuration
public class JackSonConf {

	@Bean
	public ObjectMapper getObjectMapper(){
		ObjectMapper objectMapper = new ObjectMapper();
		objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
		return objectMapper;
	}
}

    上述Java Config 會使用Spring Boot 使用自定義的Jackson 來序列化而非預設配置的。以下是一個用來獲取當前時間的請求:

package com.scg.springboot.controller;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/json")
public class JackSonController {

	@GetMapping("/user/{id}.json")
	public @ResponseBody Map<String,Date> now(){
		Map<String,Date> mp = new HashMap<String,Date>();
		mp.put("time", new Date());
		return mp;
	}
}

    當用戶訪問now.json 的時候,會得到如下輸出:

{“time”:“2018-10-23 20:50:05”}

RedirectForward

        有些情況下,Controller 會返回客戶端一個HTTP Redirect重定向請求,希望客戶端按照指定地址重新發起一次請求,比如客戶端登入成功後,重定向到後臺系統首頁。再比如客戶端通過POST 提交了一個名單,可以返回一個重定向請求到此訂單明顯的請求地址。這樣做的好處是,如果使用者再次重新整理頁面,則訪問的是訂單詳情地址,而不會再次提交訂單。

    Controller 中重定向可以返回以“redirect:” 為字首的URI :

@RequestMapping("/order/saveorder.html")
public String saveOrder(Order order){
	Long orderId = service.addOrder(order);
	return "redirect:/order/detail.html?orderId="+orderId;
}

    還可以在ModelAndView物件中設定帶有“重定向:” 字首的URI:

ModelAndView view = new ModelAndView("redirect:/order/detail.html?orderId="+orderId);

或者直接使用RedirectView的類:

RedirectView view = new RedirectView("/order/detail.html?orderId="+orderId);

Spring MVC 也支援forward 字首,用來在Controller 執行完畢後,再執行另外一個Controller 的方法。

@RequestMapping("/bbs")
public String index(){
	//forward 到 module方法
	return "forward:/bbs/module/1-1.html";
}
@RequestMapping("/bbs/moudle/{type}-{page}")
public ModelAndView module(@PathVariable int type,@PathVariable int page){
	……
}

    對所有訪問/ bbs 的請求,都會forward 到模組方法,因為forward 的URL 是/bbs/module/1-1.html ,正好匹配模組方法的@RequestMapping 的定義。