13、構(gòu)建Spring Web應(yīng)用程序(1)(spring筆記)

一、Spring MVC起步

1.1 跟蹤Spring MVC的請求

1

在請求離開瀏覽器時(shí)(步驟1),會(huì)帶有用戶所請求內(nèi)容的信息,至少會(huì)包含請求的URL,但是還可能帶有其他信息。

  • 請求的第一站是前端控制器DispatcherServletSpringMVC所有的請求都會(huì)通過這個(gè)前端控制器,前端控制器是常用的Web應(yīng)用程序模式,在這里是一個(gè)單實(shí)例的Servlet將請求委托給應(yīng)用程序的其他組件來執(zhí)行實(shí)際的處理。

  • DispatcherServlet的任務(wù)是將請求轉(zhuǎn)發(fā)給控制器(Controller)。為了達(dá)到此目的,DispatcherServlet需要查詢一個(gè)或多個(gè)處理器映射器(handler mapping)(步驟2)來確定請求所需要的控制器是哪一個(gè)(因?yàn)榭刂破鲿?huì)有很多),處理器映射器會(huì)根據(jù)請求所攜帶的URL信息進(jìn)行決策。

  • 一旦選擇了合適的控制器,DispatcherServlet會(huì)將請求發(fā)送給選中的控制器(步驟3)。到了控制器,請求會(huì)卸下其負(fù)載(用戶 提交的信息)并耐心等待控制器處理這些信息。

  • 控制器在完成邏輯處理后,通常會(huì)產(chǎn)生一些信息,這些信息需要返回給用戶并在瀏覽器上顯示。這些信息被稱為模型(model)。不過,僅僅給用戶返回原始的信息是不夠的——這些信息需要以用戶友好的方式進(jìn)行格式化,一般會(huì)是HTML。所以,信息需要發(fā)送給一個(gè)視圖(view),通常是JSP

  • 控制器做的最后一件事就是將模型數(shù)據(jù)打包,并且標(biāo)識(shí)出用于渲染輸出的視圖名。然后發(fā)送回DispatcherServlet(步驟4)。

  • 而返回給DispatcherServlet的視圖名并不直接表示某個(gè)特定的JSP,可能并不是JSP。相反,它僅僅傳遞的是一個(gè)邏輯名稱,這個(gè)名稱用來查找產(chǎn)生結(jié)果的真正視圖。這就需要使用視圖解析器對邏輯視圖名進(jìn)行解析(步驟5)。

  • 解析完之后就會(huì)找到相關(guān)視圖對數(shù)據(jù)進(jìn)行渲染(步驟6),在這里交付模型數(shù)據(jù),之后對用戶響應(yīng)(步驟7)。

1.2 搭建Spring MVC

1.2.1 配置DispatcherServlet

按照傳統(tǒng)方式,像DispatcherServlet這樣的Servlet會(huì)配置在web.xml文件中。但是借助Servlet3.1Spring3.1的功能增強(qiáng),這里使用JavaDispatcherServlet配置在Servlet容器中。

package spittr.config;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import spittr.web.WebConfig;

public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
  
  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new Class<?>[] { RootConfig.class };
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] { WebConfig.class };
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/" };
  }
}

說明:

  • 這里我們創(chuàng)建的應(yīng)用的名稱為Spittr。要理解上述代碼是如何工作的,可能只需要知道擴(kuò)展AbstractAnnotationConfigDispatcherServletInitializer的任意類都會(huì)自動(dòng)地配置DispatcherServletSpring應(yīng)用上下文,Spring的應(yīng)用上下文位于應(yīng)用程序的Servlet上下文之中。

  • AbstractAnnotationConfigDispatcherServletInitializer剖析:
    Servlet3.0環(huán)境中,容器會(huì)在類路徑中查找實(shí)現(xiàn)了javax.servlet.ServletContainerInitializer接口的類,如果能發(fā)現(xiàn)的話,就用它來配置Servlet容器。Spring提供了這個(gè)接口的實(shí)現(xiàn),名為SpringServletContainerInitializer,這個(gè)類反過來會(huì)查找WebApplicationInitializer的類并將配置的任務(wù)交給它們來完成。Spring3.2引入了一個(gè)便利的WebApplicationInitializer基礎(chǔ)實(shí)現(xiàn),即AbstractAnnotationConfigDispatcherServletInitializer。因?yàn)槲覀兊?code>SpitterWebInitializer擴(kuò)展了AbstractAnnotationConfigDispatcherServletInitializer(同時(shí)也就實(shí)現(xiàn)了WebApplicationInitializer),因此部署到Servlet3.0容器中的時(shí)候,容器會(huì)自動(dòng)發(fā)現(xiàn)它,并用它來配置Servlet上下文。

  • 這里的第一個(gè)方法getServletMappings(),它會(huì)將一個(gè)或多個(gè)路徑映射到DispatcherServlet上。本例中,它映射的是“/”,這表示它會(huì)是應(yīng)用的默認(rèn)Servlet。會(huì)處理進(jìn)入應(yīng)用的所有請求。其他兩個(gè)方法在后面說明。

  • 注意:這里使用的是Java方式,和web.xml方式不同在于,應(yīng)用啟動(dòng)時(shí),容器會(huì)在類路徑下查找ServletContainerInitializer的實(shí)現(xiàn)(SpringServletContainerInitializer),此實(shí)現(xiàn)又查找WebApplicationInitializer的實(shí)現(xiàn)(AbstractAnnotationConfigDispatcherServletInitializer),這個(gè)類會(huì)創(chuàng)建DispatcherServletContextLoaderListener,而這里應(yīng)用的配置類SpitterWebInitializer繼承了AbstractAnnotationConfigDispatcherServletInitializer

1.2.2 兩個(gè)應(yīng)用上下文之間的故事

當(dāng)DispatcherServlet啟動(dòng)的時(shí)候,會(huì)創(chuàng)建Spring應(yīng)用上下文,并加載配置文件或配置類中所聲明的bean。在上述代碼中的getServletConfigClasses()方法中,我們要求DispatcherServlet加載應(yīng)用上下文時(shí),使用定義在WebConfig配置類中的bean(相關(guān)代碼在后面給出)。

但是在Spring Web應(yīng)用中,通常還會(huì)有另外一個(gè)應(yīng)用上下文。這個(gè)上下文是由ContextLoaderListener創(chuàng)建的。一般情況是,我們希望DispatcherServlet加載包含Web組件的bean,如控制器、視圖解析器以及處理器映射器,而ContextLoaderListener要加載應(yīng)用中其他的bean。這些bean通常是驅(qū)動(dòng)應(yīng)用后端的中間層和數(shù)據(jù)層組件(如IoC容器)

實(shí)際上,AbstractAnnotationConfigDispatcherServletInitializer會(huì)同時(shí)創(chuàng)建DispatcherServlet和ContextLoaderListenergetServletConfigClasses()方法返回的帶有@Configuration注解的類將會(huì)用來定義DispatcherServlet應(yīng)用上下文中的beangetRootConfgiClasses()方法返回的帶有@Configuration注解的類將會(huì)用來配置ContextLoaderListener創(chuàng)建應(yīng)用上下文中的bean

當(dāng)然我們也可以使用傳統(tǒng)的web.xml配置,但是其實(shí)沒有必要,而這種配置方式下的應(yīng)用必須部署到支持Servlet3.0的服務(wù)器中才能正常工作。

1.2.3 啟用Spring MVC

有多種方式配置DispatcherServlet,于是,啟用Spring MVC組件的方式也不僅一種。可以使用<mvc:annotation-driven>啟用注解驅(qū)動(dòng)的Spring MVC(在后面說明),這里使用Java進(jìn)行配置。

package spittr.web;
import org.springframework.context.annotation.*;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@EnableWebMvc//啟用Spring MVC
@ComponentScan("spittr.web")//啟用組件掃描
public class WebConfig extends WebMvcConfigurerAdapter {

  @Bean
  public ViewResolver viewResolver() {
    //配置JSP視圖解析器
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".jsp");
    return resolver;
  }
  
  @Override
  public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    // 配置靜態(tài)資源的處理
    configurer.enable();
  }
  
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // 在后面進(jìn)行說明
    super.addResourceHandlers(registry);
  }
}

說明:

  • 這里啟用了組件掃描,后面帶有@Controller注解的控制器會(huì)稱為掃描時(shí)的候選bean。因此,我們不需要在配置類中顯式的聲明任何控制器。

  • 接下來,我們添加了一個(gè)ViewResolver bean。更具體來講InternalResourceViewResolver。在后面會(huì)更為詳細(xì)的討論視圖解析器。在查找的時(shí)候,它會(huì)在視圖名稱上加一個(gè)特定的前綴和后綴(例如,名為home的視圖將會(huì)解析為/WEB-INF/views/home.jsp)。

  • 最后,WebConfig類還擴(kuò)展了WebMvcConfigurerAdapter并重寫了其configureDefaultServletHandling()方法。通過調(diào)用enable()方法,我們要求DispatcherServlet將對靜態(tài)資源的請求轉(zhuǎn)發(fā)到Servlet容器中默認(rèn)的Servlet上,而不是使用DispatcherServlet本身來處理此類請求。

  • 下面給出RootConfig

package spittr.config;
import java.util.regex.Pattern;
import org.springframework.context.annotation.*;
import org.springframework.core.type.filter.RegexPatternTypeFilter;
import spittr.config.RootConfig.WebPackage;

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

}

說明:這里我們使用了自動(dòng)掃描配置,在后面可以使用非Web組件來完善RootConfig。這里的因?yàn)榍懊?code>@EnableWebMvc已經(jīng)被加載了,這里使用@excludeFilters將其排除掉。

其實(shí)ContextLoaderListener會(huì)根據(jù)根配置文件RootConfig會(huì)加載相關(guān)bean,而上述配置中將EnableWebMvc類排除了,其實(shí)還應(yīng)該將spittr.web包中的類排除(修改后的代碼后面給出),因?yàn)槟莻€(gè)包中的類是由DispatcherServlet來加載的,這樣兩者就不會(huì)產(chǎn)生重復(fù)的bean了,如果產(chǎn)生了重復(fù),則優(yōu)先使用DispatcherServlet返回的bean,而ContextLoaderListener產(chǎn)生的bean無法被調(diào)用,稱為內(nèi)存泄漏。

@Configuration
@Import(DataConfig.class)
@ComponentScan(basePackages={"spittr"},
        excludeFilters={
                @Filter(type=FilterType.CUSTOM, value=WebPackage.class)
        })
public class RootConfig {
    public static class WebPackage extends RegexPatternTypeFilter {
        public WebPackage() {
            super(Pattern.compile("spittr\\.web"));
        }
    }
}

1.3 Spittr簡介

這里Spittr表示一個(gè)類似微博的應(yīng)用,其中Spitter用于表示應(yīng)用的用戶,而Spittle表示用戶發(fā)布的簡短狀態(tài)更新。

二、編寫基本的控制器

Spring MVC中,控制器只是方法上添加了@RequestMapping注解的類,這個(gè)注解聲明了它們所要處理的請求。這里控制器盡可能簡單,假設(shè)控制器類要處理對“/”的請求,并渲染應(yīng)用的首頁。

package spittr.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class HomeController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home(){
        return "home";
    }
}

說明:

  • 很顯然,這個(gè)類帶有@Controller注解,表示這個(gè)類是一個(gè)控制器。@Controller是一個(gè)構(gòu)造型(stereotype)的注解,基于@Component注解。在這里,它的目的就是輔助實(shí)現(xiàn)組件掃描。因?yàn)檫@個(gè)類帶有@Controller注解,因此組件掃描的時(shí)候會(huì)自動(dòng)找到此類,并將其聲明為一個(gè)bean

  • HomeController有一個(gè)方法home()方法,帶有@RequestMapping注解。它的value屬性指定了這個(gè)方法所要處理的請求路徑,method屬性細(xì)化了它所處理的HTTP方法。在本例中,當(dāng)收到對“/”HTTP GET請求時(shí),就會(huì)調(diào)用此方法處理。

  • 這里home()方法返回的是一個(gè)視圖的邏輯名,之后DispatcherServlet會(huì)調(diào)用視圖解析器對其進(jìn)行解析,找到真正的視圖(/WEB-INF/views/home.jsp)。

2.1 測試控制器

以前我們經(jīng)常使用單元測試對某個(gè)方法進(jìn)行測試,但是對于這種Web應(yīng)用來說,測試總是要啟動(dòng)應(yīng)用和服務(wù)器,這是較為麻煩的。從Spring3.2開始,可以按照控制器的方式來測試控制器,而不僅僅將控制器作為一個(gè)POJO來測試。Spring現(xiàn)在包含了一種mock Spring MVC并針對控制器執(zhí)行HTTP請求的機(jī)制。這樣就不需要啟動(dòng)應(yīng)用和服務(wù)器了。

package spittr.web;
import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

public class HomeControllerTest {

    @Test
    public void testHomePage() throws Exception{
        HomeController controller = new HomeController();
        //初始化MockMvc對象
        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
        mockMvc.perform(MockMvcRequestBuilders.get("/"))
                .andExpect(MockMvcResultMatchers.view().name("home"));
    }
}

說明:這里首先使用真實(shí)控制器對象初始化MockMvc對象,然后使用perform()方法發(fā)起GET請求,然后使用view()方法檢查返回的結(jié)果是否是“name”(也就是檢查返回的邏輯視圖名是否是“name”)。

2.2 定義類級別的請求處理

之前我們是將請求定義在方法級別上(在方法上使用@RequestMapping注解),下面將請求定義在類級別上。

package spittr.web;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/")
public class HomeController {

  @RequestMapping(method = RequestMethod.GET)
  public String home(Model model) {
    return "home";
  }
}

說明:

  • 這里在控制器中,路徑現(xiàn)在被轉(zhuǎn)移到類級別的@RequestMapping上,而HTTP方法依然映射在方法級別上,此時(shí)這個(gè)注解會(huì)應(yīng)用到控制器的所有處理器方法上。當(dāng)然我們可以同時(shí)在方法上使用@RequestMapping注解,此時(shí),方法級別上的注解就是對類上注解的一種補(bǔ)充,這里兩個(gè)注解合并之后(沒有方法級別上的注解)標(biāo)明home()方法將會(huì)處理對“/”路徑的GET請求。

  • 這里注意,@RequestMappingvalue屬性能夠接收一個(gè)String類型的數(shù)組。

@Controller
@RequestMapping({"/", "/homepage"})
public class HomeController {
...
}

此時(shí),控制器的home()方法能夠映射到對“/”“/homepage”GET請求。

2.3 傳遞模型數(shù)據(jù)到視圖中

大多數(shù)控制器可能并不像上面那樣簡單,在Spittr應(yīng)用中,我們需要一個(gè)頁面展現(xiàn)最近提交的Spittle列表。因此,我們需要一個(gè)新的方法來處理這個(gè)頁面。

為了避免對數(shù)據(jù)庫進(jìn)行訪問,這里定義一個(gè)數(shù)據(jù)訪問的Repository接口,稍后實(shí)現(xiàn)它。

package spittr.data;
import java.util.List;
import spittr.Spittle;

public interface SpittleRepository {

  List<Spittle> findSpittles(long max, int count);
}

說明:findSpittles()方法接受兩個(gè)參數(shù)。其中max參數(shù)代表所返回的Spittle中,Spittle ID屬性的最大值,而count參數(shù)表明要返回多少個(gè)Spittle對象。為了獲得最新的20個(gè)Spittle對象,可以這樣使用:

List<Spittle> recent = spittleRepository.findSpittles(Long.MAX_VALUE, 20);

下面給出Spittle對象:

package spittr;
import java.util.Date;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

public class Spittle {

  private final Long id;
  private final String message;
  private final Date time;
  private Double latitude;//經(jīng)度
  private Double longitude;//緯度

  public Spittle(String message, Date time) {
    this(null, message, time, null, null);
  }
  
  public Spittle(Long id, String message, Date time, Double longitude, Double latitude) {
    this.id = id;
    this.message = message;
    this.time = time;
    this.longitude = longitude;
    this.latitude = latitude;
  }

  public long getId() {
    return id;
  }

  public String getMessage() {
    return message;
  }

  public Date getTime() {
    return time;
  }
  
  public Double getLongitude() {
    return longitude;
  }
  
  public Double getLatitude() {
    return latitude;
  }
  
  @Override
  public boolean equals(Object that) {
    return EqualsBuilder.reflectionEquals(this, that, "id", "time");
  }
  
  @Override
  public int hashCode() {
    return HashCodeBuilder.reflectionHashCode(this, "id", "time");
  }
}

說明:這里唯一要注意的是使用了Apache Common Lang包實(shí)現(xiàn)equals()hashCode()方法。下面對新的控制器進(jìn)行測試。

@Test
public void houldShowRecentSpittles() throws Exception {
    List<Spittle> expectedSpittles = createSpittleList(20);
    SpittleRepository mockRepository = Mockito.mock(SpittleRepository.class);
    Mockito.when(mockRepository.findSpittles(Long.MAX_VALUE, 20))
            .thenReturn(expectedSpittles);

    SpittleController controller = new SpittleController(mockRepository);
    MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller)
            .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
            .build();

    mockMvc.perform(MockMvcRequestBuilders.get("/spittles"))
            .andExpect(MockMvcResultMatchers.view().name("spittles"))
            .andExpect(MockMvcResultMatchers.model().attributeExists("spittleList"))
            .andExpect(MockMvcResultMatchers.model().attribute("spittleList",
                    Matchers.hasItems(expectedSpittles.toArray())));


}

說明:這里需要導(dǎo)入mockito-all-2.0.2-beta.jarhamcrest-all-1.3.jar兩個(gè)包。這里先是使用SpittleRepository接口創(chuàng)建了一個(gè)mock實(shí)例,之后的when(XXX).thenReturn(XXX)其實(shí)是一種配置,即調(diào)用某個(gè)方法應(yīng)該返回什么值。然后使用mock對象構(gòu)造一個(gè)控制器,而這里在MockMvc構(gòu)造器上調(diào)用setSingleView()。這樣的話,mock框架就不用解析控制器中的試圖名了。在很多場景中,其實(shí)沒有必要這樣做。但是對于這個(gè)控制器方法,試圖名與請求路徑非常相似,這樣如果按照默認(rèn)的視圖解析規(guī)則,MockMvc就會(huì)發(fā)生失敗,因?yàn)闊o法區(qū)分視圖路徑和控制器路徑。之后使用perform()方法發(fā)起GET請求,同時(shí)對視圖和模型都設(shè)置了一些條件,即首先斷言試圖名為“spittles”,同時(shí)斷言模型中有key“spittleList”的對象同時(shí)對象中包含相關(guān)預(yù)期內(nèi)容。在運(yùn)行測試之前還需要給出相關(guān)控制器:

package spittr.web;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import spittr.web.Spittle;
import spittr.web.SpittleRepository;

@Controller
@RequestMapping("/spittles")
public class SpittleController {

    private static final String MAX_LONG_AS_STRING = "9223372036854775807";
    private SpittleRepository spittleRepository;

    @Autowired
    public SpittleController(SpittleRepository spittleRepository) {
        this.spittleRepository = spittleRepository;
    }

    @RequestMapping(method=RequestMethod.GET)
    public String spittles(Model model) {
        model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE, 20));
        return "spittles";
    }
}

說明:

  • 這里在構(gòu)造器中注入spittleRepository。需要注意的是,在spittles()方法中定義了一個(gè)Model作為參數(shù)。這樣,此方法就能將Repository中獲取到的Spittle列表填充到模型中。Model實(shí)際上就是一個(gè)Map,它會(huì)傳遞給視圖,這樣數(shù)據(jù)就能渲染到客戶端了。當(dāng)調(diào)用addAttribute()方法并且不指定key的時(shí)候,那么key會(huì)根據(jù)值的對象類型推斷。在本例中,因?yàn)樗且粋€(gè)List<Spittle>,因此,key推斷為spittleList。最后返回視圖名spittles

  • 當(dāng)然在使用addAttribute()方法的時(shí)候也可以指定key

@RequestMapping(method=RequestMethod.GET)
public String spittles(Model model) {
  model.addAttribute("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
  return "spittles";
}
  • 如果你希望使用非Spring類型的話,那么可以使用Map來代替Model
@RequestMapping(method=RequestMethod.GET)
public String spittles(Map model) {
  model.put("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
  return "spittles";
}
  • 還有另一種方式來編寫spittles()方法:
@RequestMapping(method=RequestMethod.GET)
public List<Spittle> spittles(
    return spittleRepository.findSpittles(Long.MAX_VALUE, 20);
}

當(dāng)處理器方法像這樣返回對象或集合時(shí),這個(gè)返回值會(huì)放到模型中,模型key會(huì)根據(jù)其類型推斷得出(此處為spittleList),而邏輯視圖名會(huì)根據(jù)請求路徑推斷出,因?yàn)檫@個(gè)方法處理針對“/spittles”,于是邏輯視圖名為spittles。下面給出/WEB-INF/views/spittles.jsp

<c:forEach items="${spittleList}" var="spittle" >
  <li id="spittle_<c:out value="spittle.id"/>">
    <div class="spittleMessage"><c:out value="${spittle.message}" /></div>
    <div>
      <span class="spittleTime"><c:out value="${spittle.time}" /></span>
      <span class="spittleLocation">(<c:out value="${spittle.latitude}" />,
      <c:out value="${spittle.longitude}" />)</span>
    </div>
  </li>

此處使用了一些JSTL表達(dá)式。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容