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只管理三層架構中的展現層」,
而數據訪問層則是由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服務
- 靜態資源映射
【範例】
- 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 處理文件放置目錄
}
}
- 攔截器
- 攔截器可對每個請求前、後進行處理(類似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());
}
}
- @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);
}
}
- 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");
}
- 路徑匹配參數配置
- Spring MVC預設中,路徑參數帶".","."後面的值會被忽略,如http://localhost:8081/testProject/index/xx.yy,則會忽略.yy
- 透過覆寫congifurePathMatch方法,可設定為不忽略"."以後的參數
【範例】
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(false);
}
【高級配置】
- 文件上傳
- 增加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";
}
}
}
- HttpMessageConverter
處理request及response中的數據
HttpMessageConverter可在需統一對返回值的Json進行加密時使用,因需在reponse輸出前處理,
所以無法使用攔截器(Interceptor)的postHandler處理(此方法在返回數據之後、渲染頁面之前)
所以無法使用攔截器(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();
}
- 服務器推送
- 服務器推送有三種方法
- WebSocket(本章節不討論)
- SSE:Server Sent Event,新式瀏覽器支援
- Servlet 3.0+ 異步方法,跨瀏覽器
【範例】SSE
SSE概念為透過媒體類型,告知html所需交換的資料類型為streaming,
和WebSocket不同之處在於,WebSocket為雙向溝通,SSE單純只由Server傳送訊息,
下載較大檔案時,也是用SSE做處理
和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();
}
}