【Spring Boot實戰】Spring MVC 基礎

Spring MVC使用

  • Spring MVC概述
  • 項目快速搭建
  • 常用註解
  • 基本配置
  • 高級配置
  • 測試

【Spring MVC概述】

  • MVC和三層架構並不相同
    • MVC:Model、View、Controller
    • 三層架構:Presentation tier 展現層、Application tier 應用層、Data tier 數據訪問層
    • 三層架構是分層式的軟體體系架構,可適用於任一項目
    • MVC是設計模式,根據項目的具體需求,決定是否適用於該項目
書中作者提到「MVC只存在三層架構的展現層」,但我認為這句話,對也不對,
應該說「Spring MVC只管理三層架構中的展現層」,
而數據訪問層則是由Hibernate或MyBatis做管理,Spring則是整個三層架構中的管家,
MVC和三層架構的關係,須將MVC拆成「View、Controller、Service、Dao」,
絕對不是直接將MVC和三層架構做對應,
關係的對應為「展現層:View、Controller;應用層:Service;數據訪問層:Dao」。
附上參考網址:每日頭條 - MVC與三層架構

【項目快速搭建】

  • Spring MVC透過DispatcherServlet開發Web應用
  • 實現WebApplicationInitializer,等同於web.xml配置

【範例】

  • Configuration
@Configuration
@EnableWebMvc // 開啟MVC默認配置,如:ViewResolver、MessageConverter
@ComponentScan("com.myPackage.springmvc4")
public class MyMvcConfig {

	@Bean
	public InternalResourceViewResolver viewResolver() {
		InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
		viewResolver.setPrefix("/WEB-INF/classes/views/"); // 運行時的目錄結構
		viewResolver.setSuffix(".jsp");
		viewResolver.setViewClass(JstlView.class);
		return viewResolver;
	}
}
ViewResolver負責視圖渲染,若使用Thymeleaf則不需做此設定
  • Initializer
// 實現此介面,會自動被SpringServletContainerInitializer獲取(啟動Spring容器用)
public class WebInitializer implements WebApplicationInitializer { 

	@Override
	public void onStartup(ServletContext servletContext)
			throws ServletException {
		AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
        ctx.register(MyMvcConfig.class);
        ctx.setServletContext(servletContext); // 和當前servletContext關聯
        
        Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(ctx)); // 註冊DispatcherServlet
        servlet.addMapping("/");
        servlet.setLoadOnStartup(1);
	}
}
  • Controller
@Controller // 聲明此類別為Controller
public class HelloController {
	
	@RequestMapping("/index") // 透過RequestMapping,配置URL和方法之間的映射
	public  String hello(){
		
		return "index"; // 透過ViewResolver配置,可以得到放置路徑為/WEB-INF/classes/views/index.jsp
		
	}
}

【常用註解】

  • @Controller:聲明類別為Controller,將Web請求映射到@RequestMapping裡
  • @RequestMapping:映射Web請求、處理類別、方法,支持Servlet的request和response作為參數,也支持對媒體類型作配置
  • @ResponseBody:可返回數據而非頁面,可放置在回傳值或方法上
  • @RequestBody:允許請求參數在請求中,放置在參數前
  • @PathVarible:接收路徑參數,放置在參數前
  • @RequestParam:此註解可寫可不寫,以key/value方式接收參數
@RequestParam配合Date型態參數時,可使用@DateTimeFormat(pattern="yyyy-MM-dd")配合轉型
  • @RestController:組合@Controller、@ResponseBody,只開發和頁面交換數據時,可使用此註解(若需回傳頁面,則不需要)

【範例】

  • Bean
public class DemoObj {
	private Long id;
	private String name;
	
	public DemoObj() {
		super();
	}
	public DemoObj(Long id, String name) {
		super();
		this.id = id;
		this.name = name;
	}
	public Long getId() {
		return id;
	}
	public void setId(Long id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
}
  • Controller
@Controller
@RequestMapping("/anno") // 設定路徑為/anno
public class DemoAnnoController {
	
	// produces可設定回傳資料類型,json設定為"application/json;charset=UTF-8"
	// 訪問路徑未設定,延續類別路徑:/anno
	@RequestMapping(produces = "text/plain;charset=UTF-8")	
	// @RequestMapping註解之方法,可接受HttpServletRequest、HttpServletResponse為參數
	// @ResponseBody放在回傳值前
	public @ResponseBody String index(HttpServletRequest request) { 
		return "url:" + request.getRequestURL() + " can access";
	}

	// 訪問路徑為/anno/pathvar/xx
	@RequestMapping(value = "/pathvar/{str}", produces = "text/plain;charset=UTF-8")
	// 透過@PathVariable,接收路徑中的{str}為參數
	public @ResponseBody String demoPathVar(@PathVariable String str,
			HttpServletRequest request) {
		return "url:" + request.getRequestURL() + " can access,str: " + str;
	}

	// 訪問路徑為/anno/requestParam?id=1
	@RequestMapping(value = "/requestParam", produces = "text/plain;charset=UTF-8")
	// 也可在Long id前,加上@RequestParam
	public @ResponseBody String passRequestParam(Long id,
			HttpServletRequest request) {
		return "url:" + request.getRequestURL() + " can access,id: " + id;

	}

	// 訪問路徑為/anno/obj?id=1&name=xx
	@RequestMapping(value = "/obj", produces = "application/json;charset=UTF-8")
	// @ResponseBody也可放在方法前
	@ResponseBody
	public String passObj(DemoObj obj, HttpServletRequest request) {
		 return "url:" + request.getRequestURL() 
		 			+ " can access, obj id: " + obj.getId()+" obj name:" + obj.getName();
	}

	// 訪問路徑為/anno/name1 或 /anno/name2,可映射到不同路徑
	@RequestMapping(value = { "/name1", "/name2" }, produces = "text/plain;charset=UTF-8")
	public @ResponseBody String remove(HttpServletRequest request) {
		return "url:" + request.getRequestURL() + " can access";
	}
}
  • RestController
@RestController // 不需寫@ResponseBody
@RequestMapping("/rest")
public class DemoRestController {
	
	@RequestMapping(value = "/getjson", produces={"application/json;charset=UTF-8"})
	public DemoObj getjson (DemoObj obj){
		return new DemoObj(obj.getId()+1, obj.getName()+"yy");
	}
	
	@RequestMapping(value = "/getxml", produces={"application/xml;charset=UTF-8"})
	public DemoObj getxml(DemoObj obj){
		return new DemoObj(obj.getId()+1, obj.getName()+"yy");
	}
}

 

【基本配置】

  • 需繼承WebMvcConfigurerAdapter,使用@EnableWebMvc,開啟Spring MVC服務
  1. 靜態資源映射

【範例】

  • Configuration
@Configuration
@EnableWebMvc // 開啟Spring MVC功能
@ComponentScan("com.myPackage.highlight_springmvc4")
public class MyMvcConfig extends WebMvcConfigurerAdapter { // 需繼承此類

	@Bean
	public InternalResourceViewResolver viewResolver() {
		InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
		viewResolver.setPrefix("/WEB-INF/classes/views/");
		viewResolver.setSuffix(".jsp");
		viewResolver.setViewClass(JstlView.class);
		return viewResolver;
	}

	@Override
	// 覆寫addResourceHandlers,重新配置資源映射
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("/assets/**").addResourceLocations(
				"classpath:/assets/");
				// addResourceHandler 處理外部訪問路徑
				// addResourceLocations 處理文件放置目錄
	}
}
  1. 攔截器
  • 攔截器可對每個請求前、後進行處理(類似Filter)
  • 實現HandlerInterceptor或繼承HandlerInterceptorAdapter即可成為攔截器
  • Configuration覆寫addInterceptors方法,註冊攔截器

【範例】

  • 自定義攔截器
public class DemoInterceptor extends HandlerInterceptorAdapter { // 繼承HandlerInterceptorAdapter,實現攔截器功能
	// 也可實現HandlerInterceptor介面

	@Override
	public boolean preHandle(HttpServletRequest request, // 覆寫preHandle方法,在請求發生前執行
			HttpServletResponse response, Object handler) throws Exception {
		long startTime = System.currentTimeMillis();
		request.setAttribute("startTime", startTime);
		return true;
	}

	@Override
	public void postHandle(HttpServletRequest request, // 覆寫postHandle方法,在請求發生後執行
			HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		long startTime = (Long) request.getAttribute("startTime");
		request.removeAttribute("startTime");
		long endTime = System.currentTimeMillis();
		System.out.println("本次請求處理時間為:" + new Long(endTime - startTime)+"ms");
		request.setAttribute("handlingTime", endTime - startTime);
	}

}
  • Configuration
@ComponentScan("com.myPackage.highlight_springmvc4")
public class MyMvcConfig extends WebMvcConfigurerAdapter {

	@Bean
	public InternalResourceViewResolver viewResolver() {
		InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
		viewResolver.setPrefix("/WEB-INF/classes/views/");
		viewResolver.setSuffix(".jsp");
		viewResolver.setViewClass(JstlView.class);
		return viewResolver;
	}

	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("/assets/**").addResourceLocations(
				"classpath:/assets/");
	}

	@Bean
	// 配置攔截器
	public DemoInterceptor demoInterceptor() {
		return new DemoInterceptor();
	}

	@Override
	public void addInterceptors(InterceptorRegistry registry) { // 覆寫addInterceptors方法,註冊攔截器
		registry.addInterceptor(demoInterceptor());
	}

}
  1. @ControllerAdvice
  • 放置Controller的全局配置
  • @Controller可使用@ExceptionHadnler、@InitBinder、@ModelAttribute
    • @ExceptionHadnler:處理Controller中的異常
    • @InitBinder:設置WebDataBinder,自動綁定前台請求參數到model中
    • @ModelAttribute:可綁定key / value到model裡,設定在@ControllerAdvice,可使所有的@RequestMapping獲得此處的key/value

【範例】

  • ControllerAdvice
@ControllerAdvice // 聲明為Controller全局配置
public class ExceptionHandlerAdvice { 

	@ExceptionHandler(value = Exception.class) // 攔截所有Exception,value可設定攔截條件
	public ModelAndView exception(Exception exception, WebRequest request) {
		ModelAndView modelAndView = new ModelAndView("error");
		modelAndView.addObject("errorMessage", exception.getMessage());
		return modelAndView;
	}

	@ModelAttribute // 添加屬性至所有RequestMapping
	public void addAttributes(Model model) {
		model.addAttribute("msg", "額外訊息"); 
	}

	@InitBinder // 設置WebDataBinder
	public void initBinder(WebDataBinder webDataBinder) {
		webDataBinder.setDisallowedFields("id"); // 忽視request中的id
	}
}
  • Controller
@Controller
public class AdviceController {
	@RequestMapping("/advice")
	public String getSomething(@ModelAttribute("msg") String msg,DemoObj obj){ // 可直接使用@ModelAttribute
		
		throw new IllegalArgumentException("參數有誤" + msg);
	}

}
  1. ViewController
  • 在Configuration中,覆寫addViewControllers方法,簡化頁面控制的配置

【範例】

  • 原先的頁面控制,透過PageController控制路徑,以及要跳去哪個頁面
@RequsetMapping("/testIndex")
public String index(){
    return "index";
}
  • 透過Congifuration簡化
@Override
// addViewController對應映射路徑
// setViewName對應需映射的檔案
public void addViewControllers(ViewControllerRegistry registry) {
	registry.addViewController("/testIndex").setViewName("/index");
}
  1. 路徑匹配參數配置
  • Spring MVC預設中,路徑參數帶".","."後面的值會被忽略,如http://localhost:8081/testProject/index/xx.yy,則會忽略.yy
  • 透過覆寫congifurePathMatch方法,可設定為不忽略"."以後的參數

【範例】

 @Override
 public void configurePathMatch(PathMatchConfigurer configurer) {
 	configurer.setUseSuffixPatternMatch(false);
 }

 

【高級配置】

  1. 文件上傳
  • 增加MultipartResolver配置
  • 單一文件使用MultipartFile接收,多個文件使用MultipartFile[] 接收

【範例】

  • Configuration
@Bean
public MultipartResolver multipartResolver() {
	CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
	multipartResolver.setMaxUploadSize(1000000);
	return multipartResolver;
}
  • Controller
@Controller
public class UploadController {
	
	@RequestMapping(value = "/upload",method = RequestMethod.POST)
	public @ResponseBody String upload(MultipartFile file) { // 使用MultipartFile接收檔案
		try {
			// writeByteArrayToFile 可快速將檔案寫入
			FileUtils.writeByteArrayToFile(new File("e:/upload/"+file.getOriginalFilename()),
					file.getBytes());
			return "ok";
		} catch (IOException e) {
			e.printStackTrace();
			return "wrong";
		}
	}
}
  1. HttpMessageConverter
    處理request及response中的數據
HttpMessageConverter可在需統一對返回值的Json進行加密時使用,因需在reponse輸出前處理,
所以無法使用攔截器(Interceptor)的postHandler處理(此方法在返回數據之後、渲染頁面之前)

【範例】

  • 自定義HttpMessageConverter
// 繼承AbstractHttpMessageConverter,實現自定義的HttpMessageConverter
public class MyMessageConverter extends AbstractHttpMessageConverter<DemoObj> { 

	public MyMessageConverter() {
		// 定義媒體類型
		super(new MediaType("application", "x-wisely",Charset.forName("UTF-8")));
	}
	
	// 覆寫readInternal方法,處理request中的資料
	// 透過方法參數HttpInputMessage,可取得request中的資料
	@Override
	protected DemoObj readInternal(Class<? extends DemoObj> clazz,
			HttpInputMessage inputMessage) throws IOException,
			HttpMessageNotReadableException {
		String temp = StreamUtils.copyToString(inputMessage.getBody(),

		Charset.forName("UTF-8"));
		String[] tempArr = temp.split("-");
		return new DemoObj(new Long(tempArr[0]), tempArr[1]);
	}
	
	// 覆寫supports方法,表明只處理DemoObj類別
	@Override
	protected boolean supports(Class<?> clazz) {
		return DemoObj.class.isAssignableFrom(clazz);
	}
	
	// 覆寫writeInternal方法,處理response輸出的資料
	@Override
	protected void writeInternal(DemoObj obj, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {
		String out = "hello:" + obj.getId() + "-"
				+ obj.getName();
		outputMessage.getBody().write(out.getBytes());
	}

}
  • Configuration
// 覆寫extendMessageConverters方法,添加新的HttpMessageConverter
// 另外可覆寫configureMessageConverters方法,會覆蓋掉原本默認的多個HttpMessageConverter
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
	converters.add(converter());
}

@Bean 
public MyMessageConverter converter(){
	return new MyMessageConverter();
}
  1. 服務器推送
  • 服務器推送有三種方法
    1. WebSocket(本章節不討論)
    2. SSE:Server Sent Event,新式瀏覽器支援
    3. Servlet 3.0+ 異步方法,跨瀏覽器

【範例】SSE

SSE概念為透過媒體類型,告知html所需交換的資料類型為streaming,
和WebSocket不同之處在於,WebSocket為雙向溝通,SSE單純只由Server傳送訊息,
下載較大檔案時,也是用SSE做處理
  • Controller
@Controller
public class SseController {
	
	// 輸出的媒體類型需寫text/event-stream
	@RequestMapping(value="/push",produces="text/event-stream")
	public @ResponseBody String push(){
		Random r = new Random();
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}   
		return "data:Testing 1,2,3" + r.nextInt() +"\n\n";
	}

}
  • jsp
<script type="text/javascript">
	if (!!window.EventSource) { // 判斷瀏覽器是否支援SSE
		var source = new EventSource('push');  // push為url
		s='';
		source.addEventListener('message', function(e) { // message為接收到SSE訊息時的事件
			s+=e.data+"<br/>";
			$("#msgFrompPush").html(s);
	     
		});

		source.addEventListener('open', function(e) {
			console.log("連接打開");
		}, false);

		source.addEventListener('error', function(e) {
			if (e.readyState == EventSource.CLOSED) {
				console.log("連接關閉");
			} else {
				console.log(e.readyState);    
			}
		}, false);
	} else {
		console.log("瀏覽器不支援");
	} 
</script>

【範例】Servlet 3.0+ 異步方法

Servlet 3.0 以前,採用Thread-Per-Request的方式處理請求,每一次的Http請求都由某一個線程從頭負責到尾
  • WebInitializer
public class WebInitializer implements WebApplicationInitializer {

	@Override
	public void onStartup(ServletContext servletContext)
		throws ServletException {
		AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
		ctx.register(MyMvcConfig.class);
		ctx.setServletContext(servletContext); 
        
		Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(ctx));
		servlet.addMapping("/");
		servlet.setLoadOnStartup(1);
		servlet.setAsyncSupported(true); // 開啟異步方法支持

	}

}
  • Controller
@Controller
public class AysncController {
	@Autowired
	PushService pushService; // 由自定義的PushService取得DeferredResult

	@RequestMapping("/defer")
	@ResponseBody
	public DeferredResult<String> deferredCall() { // 透過返回DeferredResult達成異步任務的功能
		return pushService.getAsyncUpdate();
	}

}
  • Service
@Service
public class PushService {
	private DeferredResult<String> deferredResult; // 產生DeferredResult給Controller使用

	public DeferredResult<String> getAsyncUpdate() {
		deferredResult = new DeferredResult<String>();
		return deferredResult;
	}

	@Scheduled(fixedDelay = 5000)
	public void refresh() {
		if (deferredResult != null) {
			deferredResult.setResult(new Long(System.currentTimeMillis())
				.toString());
		}
	}
}

 

【測試】

測試Web時,不需要啟動,只需要Servlet相關的模擬對象,如:

  • MockMVC
  • MockHttpServletRequest
  • MockHttpServletReaponse
  • MockHttpSession

【範例】

  • Service
@Service
public class DemoService {
	
	public String saySomething(){
		return "hello";
	}

}
  • TestConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {MyMvcConfig.class})
@WebAppConfiguration("src/main/resources") // 聲明此配置為WebApplicationContext,默認路徑為「src/main/webapp」
public class TestControllerIntegrationTests {
	private MockMvc mockMvc; // 模擬MVC
	
	@Autowired
	private DemoService demoService;
	
	@Autowired 
	WebApplicationContext wac;
	
	@Autowired 
	MockHttpSession session; // 模擬Session
    
	@Autowired 
	MockHttpServletRequest request; // 模擬HttpServletRequest
    
	@Before // 測試開始前的初始化工作
	public void setup() {
		mockMvc =
				MockMvcBuilders.webAppContextSetup(this.wac).build(); // 透過此方式完成初始化
		}
	
	@Test
	public void testNormalController() throws Exception{
		mockMvc.perform(get("/normal")) // 模擬提出get請求
				.andExpect(status().isOk()) // 預期狀態為200
				.andExpect(view().name("page")) // 預期view的名稱為page
				.andExpect(forwardedUrl("/WEB-INF/classes/views/page.jsp")) // 預期頁面的真正路徑
				.andExpect(model().attribute("msg", demoService.saySomething())); // 預期msg中的內容
				
	}
	
	@Test
	public void testRestController() throws Exception{
		mockMvc.perform(get("/testRest"))
			.andExpect(status().isOk())
			.andExpect(content().contentType("text/plain;charset=UTF-8"))
			.andExpect(content().string(demoService.saySomething()));
	}

}
  • Controller1
@Controller
public class NormalController {
	@Autowired
	DemoService demoService;
	

	
	@RequestMapping("/normal")
	public  String testPage(Model model){
		model.addAttribute("msg", demoService.saySomething());
		return "page";
	}

}
  • Controller2
@RestController
public class MyRestController {
	
	@Autowired
	DemoService demoService;
	
	@RequestMapping(value = "/testRest" ,produces="text/plain;charset=UTF-8")
	public @ResponseBody String testRest(){
		return demoService.saySomething();
	}

}