1. 程式人生 > 實用技巧 >《Spring實戰》學習筆記(5)——構建Spring Web應用程式

《Spring實戰》學習筆記(5)——構建Spring Web應用程式

這章主要學習一下Spring的MVC框架。Spring MVC基於模型-檢視-控制器(Model-View-Controller)模式實現,可以構建出鬆耦合的Web應用程式。


本章知識點:

  • 對映請求到Spring控制器
  • 透明地繫結表單引數
  • 校驗表單提交
一、Spring MVC起步

  看一下請求從客戶端發起,到Spring MVC中的元件,最後返回客戶段的過程。

1. 跟蹤Spring MVC的請求

請求離開瀏覽器,帶著使用者所請求內容的資訊,包括URL,表單資訊等等。請求首先到達前端控制器DispatcherServlet。

DispatcherServlet負責將請求傳送給Spring MVC控制器(controller)——用於處理請求的Spring元件

一個應用中可能有多個控制器, DispatcherServlet會查詢一個或多個處理器對映來確定請求的下一站在哪。處理器對映會根據請求的URL來進行決策。

選擇合適的控制器後,DispatcherServlet就會將請求發給選中的控制器,並等待控制器處理使用者提交的資訊。

控制器在邏輯處理後,產生的資訊需要返回給使用者並顯示在瀏覽器上,這些資訊叫做模型(model)

但僅僅返回這些原始資訊是不夠的,需要更好的方式進行格式化,一般是HTML,所以需要傳送一個檢視(view),通常是JSP。

控制器做的最後一步是講模型資料打包並標示用於渲染輸出的檢視名,然後將請求,模型,檢視名一起傳送給DispatcherServlet。

DispatcherServlet使用檢視解析器將邏輯檢視名匹配成一個特定的檢視實現(不一定是JSP)。

DispatcherServlet知道檢視實現後,交付模型資料。

檢視使用模型資料渲染輸出,通過響應物件傳遞給客戶端。

2. 搭建Spring MVC

  我們做一個戰績查詢頁面的例子。首先配置一下DispatcherServlet。

public class RecordWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    // 指定配置類
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] {RootConfig.class};
    }

    // 指定配置類
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] {WebConfig.class};
    }

    // 將DispatcherServlet對映到"/"
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

看一下上面程式碼的三個方法是幹什麼用的:

Spring引入WebApplicationInitializer的基礎實現:AbstractAnnotationConfigDispatcherServletInitializer,部署到Servlet 3.0容器的時候,容器會自動發現它並用它配置Servlet上下文。

上面例子中第三個方法getServletMappings(),將一個或多個路徑對映到DispatcherServlet上。我們對映到的是“/”上,表示他是預設Servlet。所有請求都會進入到這裡並被處理。

DispatcherServlet啟動之後會建立Spring應用上下文並且載入配置。getServletConfigClasses() 方法就是告訴DispatcherServlet建立Spring應用上下文時用哪個配置類。

Spring Web應用中通常還有另一個上下文,由ContextLoaderListener建立。getRootConfigClasses() 方法就是告訴ContextLoaderListener載入哪個配置類。

DispatcherServlet負責載入包含Web元件的bean,如控制器,檢視解析器,處理器對映。ContextLoaderListener負責載入其他驅動應用後端的中間層和資料層元件bean。

DispatcherServletContextLoaderListener會同時被AbstractAnnotationConfigDispatcherServletInitializer建立。

接下來看一下如何啟用Spring MVC:

@Configuration
@EnableWebMvc
public class WebConfig {
}

使用@EnableWebMvc啟動Spring MVC。接下來我們要解決這幾個問題:

  • 配置檢視解析器。
  • 啟用元件掃描。
  • 處理靜態資源。DispatcherServlet會對映應用預設Servlet,它會處理所有請求,我們不想讓它處理靜態資源。

我們修改一下WebConfig:

@Configuration
@EnableWebMvc
@ComponentScan("chapterfive")  // 掃描chapterfive包來查詢元件
public class WebConfig extends WebMvcConfigurerAdapter {

    // ViewResolver bean會查詢JSP檔案,並且在檢視名稱上加上特定的字首字尾,比如名字叫index的檢視就變成了/WEB-INF/views/index.jsp
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

    // 繼承WebMvcConfigurerAdapter並重寫configureDefaultServletHandling()方法。目的是讓DispatcherServlet將對靜態資源的請求轉發到Servlet容器中預設的Servlet上,而不讓它自身處理。
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

}

配置RootConfig:

@Configuration
@ComponentScan(basePackages = {"chapterfive"}, excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)})
public class RootConfig {

}
二、編寫基本的控制器
@Controller  // 宣告為控制器
public class IndexController {
    @RequestMapping(value = "/", method = RequestMethod.GET) // 處理對"/"的GET請求
    public String index() {
        return "index";  // 檢視名為index
    }
}

@Controller註解基於@Component,所以元件掃描會自動找到IndexController,並宣告為bean。方法返回的“index”將會被Spring MVC解讀為要渲染的檢視名。根據之前的配置,這個檢視名會被解析為/WEB-INF/views/index.jsp。

1. 測試控制器

我們寫一個index.jsp的頁面:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>戰績查詢</title>
</head>
<body>
    <h1>歡迎使用戰績查詢</h1>
    <a href="<c:url value="/login" />">登入</a>|<a href="<c:url value="/regist" />">註冊</a>
</body>
</html>

測試一下controller,要再maven裡新增

<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>7.0</version>
</dependency>

否則執行時會報Error java: 無法訪問javax.servlet.ServletException這個錯誤。

編寫測試類:

public class IndexControllerTest {
    @Test
    public void testIndex() {
        IndexController controller = new IndexController();
        assertEquals("index", controller.index());
    }
}

執行,測試通過。

我們再增加一個依賴然後改進測試類:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>
public class IndexControllerTest {
    @Test
    public void testIndex() throws Exception {
        IndexController controller = new IndexController();
        MockMvc mockMvc = standaloneSetup(controller).build();
        mockMvc.perform(MockMvcRequestBuilders.get("/")).andExpect(view().name("index")); // 執行get請求,預期得到index檢視
    }
}

測試通過。

2. 定義類級別的請求處理
@Controller  // 宣告為控制器
@RequestMapping("/")
public class IndexController {
    @RequestMapping(method = RequestMethod.GET) // 處理對"/"的GET請求
    public String index() {
        return "index";  // 檢視名為index
    }
}

路徑轉移到類級別上的@RequestMapping上,它會應用到控制器中的所有處理器方法上,在處理器方法上的@RequestMapping會對類級別的@RequestMapping宣告進行補充。

@RequestMapping的value屬性還可以接收陣列:

@RequestMapping({"/", "/indexPage"})

3. 傳遞模型資料到檢視中

  我們在首頁中顯示一個列表。首先定義一個數據訪問介面:

@Component
public interface RecordRepository {
    // count表示返回多少資料
    List<Record> getRecordList(int count);
}

實現類:

@Component
public class RecordRepositryImpl implements RecordRepository {
    @Override
    public List<Record> getRecordList(int count) {
        List<Record> list = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            list.add(new Record("name" + i, true, new Date()));
        }
        return list;
    }
public class Record {
    private String heroName;
    private boolean win;
    private Date date;

    public Record(String heroName, boolean win, Date date) {
        this.heroName = heroName;
        this.win = win;
        this.date = date;
    }

    public String getHeroName() {
        return heroName;
    }

    public void setHeroName(String heroName) {
        this.heroName = heroName;
    }

    public boolean isWin() {
        return win;
    }

    public void setWin(boolean win) {
        this.win = win;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }
}

controller中增加一個獲取列表的方法:

@Controller  // 宣告為控制器
@RequestMapping("/")
public class IndexController {

    @Autowired
    private RecordRepository recordRepository;

    @RequestMapping(method = RequestMethod.GET) // 處理對"/"的GET請求
    public String index() {
        return "index";  // 檢視名為index
    }

    @RequestMapping(value = "/getRecordList",method = RequestMethod.GET) // 處理對"/"的GET請求
    public String getRecordList(Model model) {
       model.addAttribute("list",recordRepository.getRecordList(20));
       return "index";
    }
}

JSP中使用<c:forEach>標籤渲染列表。

三、接受請求的輸入

  Spring MVC允許多種方式將客戶端的資料傳到控制器的處理器方法中:

  • 查詢引數(Query Parameter)
  • 表單引數(Form Parameter)
  • 路徑變數(Path Variable)
1. 處理查詢引數

比如我們想在獲取列表的方法中,將查詢數量改為引數接收:

 @RequestMapping(value = "/getRecordList",method = RequestMethod.GET) // 處理對"/"的GET請求
    public String getRecordList(@RequestParam(value = "count",defaultValue = "20") int count, Model model) {
       model.addAttribute("list",recordRepository.getRecordList(count));
       return "index";
    }

儘管defaultValue屬性的值是String型別,但繫結到count上時,會自動轉換成int型別。

請求使用:/getRecordList?count=20

2. 通過路徑引數接收
@RequestMapping(value = "/getRecordList/{count}",method = RequestMethod.GET) // 處理對"/"的GET請求
    public String getRecordList(@PathVariable("count") int count, Model model) {
       model.addAttribute("list",recordRepository.getRecordList(count));
       return "index";
    }

請求使用:/getRecordList/20

****