天天看點

卧槽,原來SpringMVC可以這麼簡單

文章目錄

  • 認識SpringMVC
    • 一、什麼是SpringMVC?
    • 二、為什麼要使用SpringMVC?
    • 三、回顧Struts2開發
    • 四、Struts2的工作流程
    • 五、SpringMVC快速入門
      • 5.1導入開發包
      • 5.2編寫Action
      • 5.3注冊核心控制器
      • 5.4建立SpringMVC控制器
      • 5.5通路
    • 六、SpringMVC工作流程
      • 6.1映射器
      • 6.2擴充卡
      • 6.3視圖解析器
    • 七、AbstractCommandController
      • 7.1實體
      • 7.2送出參數的JSP
      • 7.3 配置Action處理請求
      • 7.4 Action接收參數
      • 7.5測試效果:
    • 八、小總結
  • 參數綁定、資料回顯、檔案上傳
    • 一、參數綁定
      • 1.1預設支援的參數類型
      • 1.2參數的綁定過程
      • 1.3自定義綁定參數【老方式、全部Action均可使用】
      • 1.4配置轉換器
      • 1.5 自定義參數轉換器【新方式、推崇方式】
      • 1.6 配置轉換器
      • 1.7 @RequestParam注解
      • 1.8 Controller方法傳回值
      • 2.1`@ModelAttribute`注解
    • 三、SpringMVC檔案上傳
      • 3.1配置虛拟目錄
      • 3.2快速入門
    • 四、總結
  • 攔截器、統一處理異常、RESTful、攔截器
    • 一、Validation
      • 1.1快速入門
      • 1.2分組校驗
    • 二、統一異常處理
      • 2.1 定義統一異常處理器類
      • 2.2配置統一異常處理器
    • 三、RESTful支援
      • 3.1url的RESTful實作
      • 3.2更改DispatcherServlet的配置
    • 四、SpringMVC攔截器
      • 4.1測試執行順序
      • 4.2攔截器應用-身份認證
    • 五、總結
    • 各類知識點總結
        • 涵蓋Java後端所有知識點的開源項目(已有8K+ star):

SpringMVC是Spring家族的一員,Spring是将現在開發中流行的元件進行組合而成的一個架構!它用在基于MVC的表現層開發,類似于struts2架構

卧槽,原來SpringMVC可以這麼簡單

我們在之前已經學過了Struts2這麼一個基于MVC的架構…那麼我們已經學會了Struts2,為啥要要學習SpringMVC呢???

下面我們來看一下Struts2不足之處:

  • 有漏洞【詳細可以去搜尋】
  • 運作速度較慢【比SpringMVC要慢】
  • 配置的内容較多【需要使用Struts.xml檔案】
  • 比較重量級

基于這麼一些原因,并且業内現在SpringMVC已經逐漸把Struts2給替代了…是以我們學習SpringMVC一方面能夠讓我們跟上業界的潮流架構,一方面SpringMVC确實是非常好用!

可以這麼說,Struts2能做的東西,SpringMVC也能夠做…

如果沒接觸過Struts2的,這裡可以跳過。Struts2可以不學

在Struts2中,我們的開發特點是這樣的:

  • Action類繼承着ActionSupport類【如果要使用Struts2提供的額外功能,就要繼承它】
  • Action業務方法總是傳回一個字元串,再由Struts2内部通過我們手寫的Struts.xml配置檔案去跳轉到對應的view
  • Action類是多例的,接收Web傳遞過來的參數需要使用執行個體變量來記住,通常我們都會寫上set和get方法

卧槽,原來SpringMVC可以這麼簡單
  • Struts2接收到request請求
  • 将請求轉向我們的過濾分批器進行過濾
  • 讀取Struts2對應的配置檔案
  • 經過預設的攔截器之後建立對應的Action【多例】
  • 執行完業務方法就傳回給response對象

如果用Maven的,那導入Maven依賴即可

前6個是Spring的核心功能包【IOC】,第7個是關于web的包,第8個是SpringMVC包

  • org.springframework.context-3.0.5.RELEASE.jar
  • org.springframework.expression-3.0.5.RELEASE.jar
  • org.springframework.core-3.0.5.RELEASE.jar
  • org.springframework.beans-3.0.5.RELEASE.jar
  • org.springframework.asm-3.0.5.RELEASE.jar
  • commons-logging.jar
  • org.springframework.web-3.0.5.RELEASE.jar
  • org.springframework.web.servlet-3.0.5.RELEASE.jar

Action實作Controller接口

public class HelloAction implements Controller {
    @Override
    public ModelAndView handleRequest(javax.servlet.http.HttpServletRequest httpServletRequest, javax.servlet.http.HttpServletResponse httpServletResponse) throws Exception {
        return null;
    }
    
}
           

我們隻要實作handleRequest方法即可,該方法已經說了request和response對象給我們用了。這是我們非常熟悉的request和response對象。然而該方法傳回的是ModelAndView這麼一個對象,這是和Struts2不同的。Struts2傳回的是字元串,而SpringMVC傳回的是ModelAndView

ModelAndView其實他就是将我們的視圖路徑和資料封裝起來而已【我們想要跳轉到哪,把什麼資料存到request域中,設定這個對象的屬性就行了】。

public class HelloAction implements Controller {
    @Override
    public ModelAndView handleRequest(javax.servlet.http.HttpServletRequest httpServletRequest, javax.servlet.http.HttpServletResponse httpServletResponse) throws Exception {


        ModelAndView modelAndView = new ModelAndView();

        //跳轉到hello.jsp頁面。
        modelAndView.setViewName("/hello.jsp");
        return modelAndView;
    }
}
           

在Struts2中,我們想要使用Struts2的功能,那麼就得在web.xml檔案中配置過濾器。而我們使用SpringMVC的話,我們是在web.xml中配置核心控制器

<!-- 注冊springmvc架構核心控制器 -->
    <servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

        <!--到類目錄下尋找我們的配置檔案-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:hello.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>DispatcherServlet</servlet-name>
        <!--映射的路徑為.action-->
        <url-pattern>*.action</url-pattern>
    </servlet-mapping>
           

我們在hello.xml配置檔案中把SpringMVC的控制器建立出來

<!--
        注冊控制器
        name屬性的值表示的是請求的路徑【也就是說,當使用者請求到/helloAction時,就交由HelloAction類進行處理】
    -->
    <bean class="HelloAction" name="/hello.action"></bean>
           

當我們在浏覽器通路

http://localhost:8080/hello.action

的時候,Spring會讀取到我們的通路路徑,然後對比一下我們的配置檔案中是否有配置

/hello.action

,如果有。那麼就交由對應的Action類來進行處理。Action類的業務方法将其請求輸出到hello.jsp頁面上。

卧槽,原來SpringMVC可以這麼簡單
卧槽,原來SpringMVC可以這麼簡單

卧槽,原來SpringMVC可以這麼簡單
  • 使用者發送請求
  • 請求交由核心控制器處理
  • 核心控制器找到映射器,映射器看看請求路徑是什麼
  • 核心控制器再找到擴充卡,看看有哪些類實作了Controller接口或者對應的bean對象
  • 将帶過來的資料進行轉換,格式化等等操作
  • 找到我們的控制器Action,處理完業務之後傳回一個ModelAndView對象
  • 最後通過視圖解析器來對ModelAndView進行解析
  • 跳轉到對應的JSP/html頁面

上面的工作流程中,我們是沒有講過映射器,擴充卡,視圖解析器這樣的東西的。但是SpringMVC的環境還是被我們搭建起來了。

下面就由我來一個一個來介紹他們是有什麼用的!

我們在web.xml中配置規定隻要是.action為字尾的請求都是會經過SpringMVC的核心Servlet。

當我們接收到請求的時候,我們發現是hello.action,是會經過我們的核心Servlet的,那麼核心Servlet就會去找有沒有專門的Action類來處理hello.action請求的。

也就是說:映射器就是用于處理“什麼樣的請求送出給Action”處理。【預設可省略的】…

其實我們在快速入門的例子已經配置了:name屬性就是規定了hello.action到HelloAction控制器中處理!

<!--
        注冊控制器
        name屬性的值表示的是請求的路徑【也就是說,當使用者請求到/helloAction時,就交由HelloAction類進行處理】
    -->
    <bean class="HelloAction" name="/hello.action"></bean>
           

映射器預設的值是這樣的:

<!-- 注冊映射器(handler包)(架構)【可省略】 -->
	  <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
 	  </bean>
           

當然了,上面我們在建立控制器的時候【也就是HelloAction】可以不使用name屬性來指定路徑,可以使用我們的映射器來配置。如以下的代碼:

<bean class="HelloAction" id="helloAction"></bean>

    <!-- 注冊映射器(handler包)(架構) -->
    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/hello.action">helloAction</prop>
            </props>
        </property>
    </bean>

           

當我們需要多個請求路徑都交由helloAction控制器來處理的話,我們隻要添加prop标簽就行了!

<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/hello.action">helloAction</prop>
                <prop key="/bye.action">helloAction</prop>
            </props>
        </property>
    </bean>
           
卧槽,原來SpringMVC可以這麼簡單

當我們映射器找到對應的Action來處理請求的時候,核心控制器會讓擴充卡去找該類是否實作了Controller接口。【預設可省略的】

也就是說:擴充卡就是去找實作了Controller接口的類

<!-- 擴充卡【可省略】 -->
    <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"></bean>
           

我們把結果封裝到ModelAndView以後,SpringMVC會使用視圖解析器來對ModelAndView進行解析。【預設可省略的】

也有一種情況是不能省略的。我們在快速入門的例子中,将結果封裝到ModelAndView中,用的是絕對真實路徑!如果我們用的是邏輯路徑,那麼就必須對其配置,否則SpringMVC是找不到對應的路徑的。

那什麼是邏輯路徑呢???我們在Struts2中,傳回的是"success"這樣的字元串,進而跳轉到success.jsp這樣的頁面上。我們就可以把"success"稱作為邏輯路徑。

在Action中傳回hello,hello是一個邏輯路徑。需要我們使用視圖解析器把邏輯路基補全

public ModelAndView handleRequest(javax.servlet.http.HttpServletRequest httpServletRequest, javax.servlet.http.HttpServletResponse httpServletResponse) throws Exception {


        ModelAndView modelAndView = new ModelAndView();

        //跳轉到hello.jsp頁面。
        modelAndView.setViewName("hello");
        return modelAndView;
    }
           

如果不使用視圖解析器的話,那麼就會找不到頁面:

卧槽,原來SpringMVC可以這麼簡單

是以,我們需要配置視圖解析器

<!--
    如果Action中書寫的是視圖邏輯名稱,那麼視圖解析器就必須配置
    如果Action中書寫的是視圖真實名稱,那麼視圖解析器就可選配置
-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!-- 路徑字首 -->
        <property name="prefix" value="/"/>
        <!-- 路徑字尾 -->
        <property name="suffix" value=".jsp"/>
        <!-- 字首+視圖邏輯名+字尾=真實路徑 -->
    </bean>
           

到目前為止,我們都沒有将SpringMVC是怎麼接收web端傳遞過來的參數的。

我們在Struts2中,隻要在Action類上寫對應的成員變量,給出對應的set和get方法。那麼Struts2就會幫我們把參數封裝到對應的成員變量中,是非常友善的。

那麼我們在SpringMVC中是怎麼擷取參數的呢????我們是将Action繼承AbstractCommandController這麼一個類的。

public class HelloAction extends AbstractCommandController {
    
    @Override
    protected ModelAndView handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, BindException e) throws Exception {
           
        return null;
    } 
}
           

在講解該控制器之前,首先我們要明白SpringMVC控制器一個與Struts2不同的地方:SpringMVC的控制器是單例的,Struts2的控制器是多例的!

也就是說:Struts2收集變量是定義成員變量來進行接收,而SpringMVC作為單例的,是不可能使用成員變量來進行接收的【因為會有多個使用者通路,就會出現資料不合理性】!

那麼SpringMVC作為單例的,他隻能通過方法的參數來進行接收對應的參數!隻有方法才能保證不同的使用者對應不同的資料!

實體的屬性要和web頁面上的name送出過來的名稱是一緻的。這和Struts2是一樣的!

public class User {

    private String id;
    private String username;

    public User() {
    }

    public User(String id, String username) {
        this.id = id;
        this.username = username;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String toString() {
        return "User{" +
                "id='" + id + '\'' +
                ", username='" + username + '\'' +
                '}';
    }
}

           

<form action="${pageContext.request.contextPath}/hello.action" method="post">
    <table align="center">
        <tr>
            <td>使用者名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>編号</td>
            <td><input type="text" name="id"></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="送出">
            </td>
        </tr>
    </table>

</form>

           

<bean class="HelloAction" id="helloAction"></bean>


    <!-- 注冊映射器(handler包)(架構) -->
    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/hello.action">helloAction</prop>
            </props>
        </property>
    </bean>
           

public class HelloAction extends AbstractCommandController {

    /*設定無參構造器,裡邊調用setCommandClass方法,傳入要封裝的對象*/
    public HelloAction() {
        this.setCommandClass(User.class);
    }

    /**
     *
     * @param httpServletRequest
     * @param httpServletResponse
     * @param o  這裡的對象就表示已經封裝好的了User對象了。!
     * @param e
     * @return
     * @throws Exception
     */
    @Override
    protected ModelAndView handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, BindException e) throws Exception {

        User user = (User) o;

        System.out.println(user);

        ModelAndView modelAndView = new ModelAndView();
        //跳轉到ok.jsp
        modelAndView.setViewName("/WEB-INF/ok.jsp");
        //将資料封裝到ModelAndView中
        modelAndView.addObject("USER", user);
        return modelAndView;
    }
}
           

卧槽,原來SpringMVC可以這麼簡單

卧槽,原來SpringMVC可以這麼簡單
卧槽,原來SpringMVC可以這麼簡單

Struts2和SpringMVC存值的差別:

卧槽,原來SpringMVC可以這麼簡單
  • SpringMVC的工作流程:
    • 使用者發送HTTP請求,SpringMVC核心控制器接收到請求
    • 找到映射器看該請求是否交由對應的Action類進行處理
    • 找到擴充卡看有無該Action類
    • Action類處理完結果封裝到ModelAndView中
    • 通過視圖解析器把資料解析,跳轉到對應的JSP頁面
  • 控制器
    • AbstractCommandController
      • 可以實作對參數資料的封裝
卧槽,原來SpringMVC可以這麼簡單

本文主要講解的知識點如下:

  • 參數綁定
  • 資料回顯
  • 檔案上傳

我們在Controller使用方法參數接收值,就是把web端的值給接收到Controller中處理,這個過程就叫做參數綁定…

從上面的用法我們可以發現,我們可以使用request對象、Model對象等等,其實是不是可以随便把參數寫上去都行???其實并不是的…

Controller方法預設支援的參數類型有4個,這4個足以支撐我們的日常開發了

  • HttpServletRequest
  • HttpServletResponse
  • HttpSession
  • Model

一般地,我們要用到自定義的參數綁定就是上面所講的日期類型轉換以及一些特殊的需求…對于平常的參數綁定,我們是無需使用轉換器的,SpringMVC就已經幫我們幹了這個活了…

卧槽,原來SpringMVC可以這麼簡單

在上一篇我們已經簡單介紹了怎麼把字元串轉換成日期類型了【使用的是WebDataBinder方式】…其實那是一個比較老的方法,我們可以使用SpringMVC更推薦的方式…

在上次把字元串轉換成日期類型,如果使用的是WebDataBinder方式的話,那麼該轉換僅僅隻能在目前Controller使用…如果想要全部的Controller都能夠使用,那麼我們可以使用WebBindingInitializer方式

如果想多個controller需要共同注冊相同的屬性編輯器,可以實作PropertyEditorRegistrar接口,并注入webBindingInitializer中。

實作接口

public class CustomPropertyEditor implements PropertyEditorRegistrar {

	@Override
	public void registerCustomEditors(PropertyEditorRegistry binder) {
		binder.registerCustomEditor(Date.class, new CustomDateEditor(
				new SimpleDateFormat("yyyy-MM-dd HH-mm-ss"), true));
		
	}

}
           

注入到webBindingInitializer中

<!-- 注冊屬性編輯器 -->
	<bean id="customPropertyEditor" class="cn.itcast.ssm.controller.propertyeditor.CustomPropertyEditor"></bean>
	
	
	<!-- 自定義webBinder -->
	<bean id="customBinder"
		class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
	
		<!-- propertyEditorRegistrars用于屬性編輯器 -->
		 <property name="propertyEditorRegistrars">
			<list>
				<ref bean="customPropertyEditor" />
			</list>
		</property>
	</bean>


	<!-- 注解擴充卡 -->
	<bean
		class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
		<!-- 在webBindingInitializer中注入自定義屬性編輯器、自定義轉換器 -->
		<property name="webBindingInitializer" ref="customBinder"></property>
	</bean>


           

上面的方式是對象較老的,現在我們一般都是實作Converter接口來實作自定義參數轉換…我們就來看看實作Converter比上面有什麼好

配置日期轉換器

public class CustomDateConverter implements Converter<String, Date> {

	@Override
	public Date convert(String source) {
		try {
			//進行日期轉換
			return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(source);
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

}
           

配置去除字元串轉換器

public class StringTrimConverter implements Converter<String, String> {

	@Override
	public String convert(String source) {
		try {
			//去掉字元串兩邊空格,如果去除後為空設定為null
			if(source!=null){
				source = source.trim();
				if(source.equals("")){
					return null;
				}
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		return source;
	}
}

           

從上面可以得出,我們想要轉換什麼内容,就直接實作接口,該接口又是支援泛型的,閱讀起來就非常友善了…

<!-- 轉換器 -->
	<bean id="conversionService"
		  class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
		<property name="converters">
			<list>
				<bean class="cn.itcast.ssm.controller.converter.CustomDateConverter"/>
				<bean class="cn.itcast.ssm.controller.converter.StringTrimConverter"/>
			</list>
		</property>
	</bean>


	<!-- 自定義webBinder -->
	<bean id="customBinder"
		class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
		<!-- 使用converter進行參數轉 -->
		<property name="conversionService" ref="conversionService" />
	</bean>


	<!-- 注解擴充卡 -->
	<bean
		class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
		<!-- 在webBindingInitializer中注入自定義屬性編輯器、自定義轉換器 -->
		<property name="webBindingInitializer" ref="customBinder"></property>
	</bean>


	
           

如果是基于

<mvc:annotation-driven>

的話,我們是這樣配置的

<mvc:annotation-driven conversion-service="conversionService">
</mvc:annotation-driven>
<!-- conversionService -->
	<bean id="conversionService"
		class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
		<!-- 轉換器 -->
		<property name="converters">
			<list>
				<bean class="cn.itcast.ssm.controller.converter.CustomDateConverter"/>
				<bean class="cn.itcast.ssm.controller.converter.StringTrimConverter"/>
			</list>
		</property>
	</bean>
           

我們一般使用的參數綁定都有遵循的規則:方法參數名要與傳遞過來的name屬性名相同。

在預設的情況下,隻有名字相同,SpringMVC才會幫我們進行參數綁定…

如果我們使用

@RequestParam注解

的話,我們就可以使方法參數名與傳遞過來的name屬性名不同…

該注解有三個變量

  • value【指定name屬性的名稱是什麼】
  • required【是否必須要有該參數】
  • defaultvalue設定預設值

例子:我們的方法參數叫id,而頁面帶過來的name屬性名字叫item_id,一定需要該參數

public String editItem(@RequestParam(value="item_id",required=true) String id) {

}
           

Controller方法的傳回值其實就幾種類型,我們來總結一下…

  • void
  • String
  • ModelAndView
  • redirect重定向
  • forward轉發
卧槽,原來SpringMVC可以這麼簡單

二、資料回顯

其實資料回顯我們現在的話就一點也不陌生了…我們剛使用EL表達式的時候就已經學會了資料回顯了,做SSH項目的時候也有三圈問題的資料回顯…

在頁面上資料回顯本質上就是擷取reqeust域的值…

而在我們SpringMVC中,我們是使用Model來把資料綁定request域對象中的

一般地我們都是使用model.addAttribute()的方式把資料綁定到request域對象中…其實SpringMVC還支援注解的方式

2.1

@ModelAttribute

注解

我們可以将請求的參數放到Model中,回顯到頁面上

卧槽,原來SpringMVC可以這麼簡單

上面這種用法和model.addAttribute()的方式是沒啥差別的,也展現不了注解的友善性…

而如果我們要回顯的資料是公共的話,那麼我們就能夠體會到注解的友善性了,我們把公共需要顯示的屬性抽取成方法,将傳回值傳回就行了。

卧槽,原來SpringMVC可以這麼簡單

那我們就不用在每一個controller方法通過Model将資料傳到頁面。

我們使用Struts2的時候,覺得Struts2的檔案上傳方式比傳統的檔案上傳方式好用多了…

既然我們正在學習SpringMVC,那麼我們也看一下SpringMVC究竟是怎麼上傳檔案的…

在這次,我們并不是把圖檔上傳到我們的工程目錄中…

那為啥不将圖檔直接上傳到我們的工程目錄中呢???我們仔細想想,按照我們之前的做法,直接把檔案上傳到工程目錄,而我們的工程目錄是我們寫代碼的地方 …往往我們需要備份我們的工程目錄。

如果把圖檔都上傳到工程目錄中,那麼就非常難以處理圖檔了…

是以,我們需要配置Tomcat的虛拟目錄來解決,把上傳的檔案放在虛拟目錄上…

又值得注意的是,Idea使用的Tomcat并不能使用傳統的配置方式,也就是修改server.xml方式來配置虛拟目錄,在Idea下好像不支援這種做法…

那麼我在網上已經找到了對應的解決辦法,就是如果在idea上配置虛拟目錄

檢測是否配置成功:

卧槽,原來SpringMVC可以這麼簡單

在SpringMVC中檔案上傳需要用到的jar包

如果用Maven的同學,引入pom就好了
  • commons-fileupload-1.2.2.jar
  • commons-io-2.4.jar

配置檔案上傳解析器

<!-- 檔案上傳 -->
    <bean id="multipartResolver"
          class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!-- 設定上傳檔案的最大尺寸為5MB -->
        <property name="maxUploadSize">
            <value>5242880</value>
        </property>
    </bean>
           

測試的JSP

<%--
  Created by IntelliJ IDEA.
  User: ozc
  Date: 2017/8/11
  Time: 9:56
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>測試檔案上傳</title>
</head>
<body>


<form action="${pageContext.request.contextPath}/upload.action" method="post" enctype="multipart/form-data" >
    <input type="file" name="picture">
    <input type="submit" value="submit">
</form>

</body>
</html>

           

值得注意的是,在JSP的name屬性寫的是picture,那麼在Controller方法參數的名稱也是要寫picture的,否則是擷取不到對應的檔案的…

@Controller
public class UploadController {
    @RequestMapping("/upload")
    //MultipartFile該對象就是封裝了圖檔檔案
    public void upload(MultipartFile picture) throws Exception {
        System.out.println(picture.getOriginalFilename());
    }
}

           
卧槽,原來SpringMVC可以這麼簡單

  • 在SpringMVC中的業務方法預設支援的參數有四種
    • request
    • response
    • session
    • model
  • 我們的參數綁定(自動封裝參數)是由我們的轉換器來進行綁定的。現在用的一般都是Converter轉換器
  • 在上一章中我們使用WebDataBinder方式來實作對日期格式的轉化,當時僅僅是可用于目前Action的。我們想要讓全部Action都可以使用的話,有兩種方式:
    • 實作PropertyEditorRegistrar(比較老的方式)
    • 實作Converter(新的方式)
  • 參數綁定都有遵循的規則:方法參數名要與傳遞過來的name屬性名相同
    • 我們可以使用@RequestParam注解來具體指定對應的name屬性名稱,這樣也是可以實作參數綁定的。
    • 還能夠配置該參數是否是必須的。
  • Controller方法的傳回值有5種:
  • Model内部就是将資料綁定到request域對象中的。
  • @ModelAttribute注解能夠将資料綁定到model中(也就是request中),如果經常需要綁定到model中的資料,抽取成方法來使用這個注解還是不錯的。
  • idea配置虛拟目其實就是加多一個deployment,然後配置它的應用路徑
  • SpringMVC的檔案上傳就是配置一個上傳解析器,使用MultipartFile來接收帶過來的檔案。
卧槽,原來SpringMVC可以這麼簡單

本博文主要講解的知識點如下:

  • 校驗器
  • 統一處理異常
  • RESTful
  • 攔截器

在我們的Struts2中,我們是繼承ActionSupport來實作校驗的…它有兩種方式來實作校驗的功能

  • 手寫代碼
  • XML配置
    • 這兩種方式也是可以特定處理方法或者整個Action的

而SpringMVC使用JSR-303(javaEE6規範的一部分)校驗規範,springmvc使用的是Hibernate Validator(和Hibernate的ORM無關)

導入jar包

卧槽,原來SpringMVC可以這麼簡單

配置校驗器

<!-- 校驗器 -->
<bean id="validator"
      class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
  <!-- 校驗器 -->
  <property name="providerClass" value="org.hibernate.validator.HibernateValidator" />
  <!-- 指定校驗使用的資源檔案,如果不指定則預設使用classpath下的ValidationMessages.properties -->
  <property name="validationMessageSource" ref="messageSource" />
</bean>
           

錯誤資訊的校驗檔案配置

<!-- 校驗錯誤資訊配置檔案 -->
	<bean id="messageSource"
		class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
		<!-- 資源檔案名 -->
		<property name="basenames">
			<list>
				<value>classpath:CustomValidationMessages</value>
			</list>
		</property>
		<!-- 資源檔案編碼格式 -->
		<property name="fileEncodings" value="utf-8" />
		<!-- 對資源檔案内容緩存時間,機關秒 -->
		<property name="cacheSeconds" value="120" />
	</bean>
           

添加到自定義參數綁定的WebBindingInitializer中

<!-- 自定義webBinder -->
	<bean id="customBinder"
		class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
		<!-- 配置validator -->
		<property name="validator" ref="validator" />
	</bean>
           

最終添加到擴充卡中

<!-- 注解擴充卡 -->
	<bean
		class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
		<!-- 在webBindingInitializer中注入自定義屬性編輯器、自定義轉換器 -->
		<property name="webBindingInitializer" ref="customBinder"></property>
	</bean>
           

建立CustomValidationMessages配置檔案

卧槽,原來SpringMVC可以這麼簡單

定義規則

package entity;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.Date;

public class Items {
    private Integer id;

    //商品名稱的長度請限制在1到30個字元
    @Size(min=1,max=30,message="{items.name.length.error}")
    private String name;

    private Float price;

    private String pic;

    //請輸入商品生産日期
    @NotNull(message="{items.createtime.is.notnull}")
    private Date createtime;

    private String detail;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name == null ? null : name.trim();
    }

    public Float getPrice() {
        return price;
    }

    public void setPrice(Float price) {
        this.price = price;
    }

    public String getPic() {
        return pic;
    }

    public void setPic(String pic) {
        this.pic = pic == null ? null : pic.trim();
    }

    public Date getCreatetime() {
        return createtime;
    }

    public void setCreatetime(Date createtime) {
        this.createtime = createtime;
    }

    public String getDetail() {
        return detail;
    }

    public void setDetail(String detail) {
        this.detail = detail == null ? null : detail.trim();
    }
}

           

測試:

<%--
  Created by IntelliJ IDEA.
  User: ozc
  Date: 2017/8/11
  Time: 9:56
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>測試檔案上傳</title>
</head>
<body>


<form action="${pageContext.request.contextPath}/validation.action" method="post" >
    名稱:<input type="text" name="name">
    日期:<input type="text" name="createtime">
    <input type="submit" value="submit">
</form>

</body>
</html>



           

Controller需要在校驗的參數上添加@Validation注解…拿到BindingResult對象…

@RequestMapping("/validation")
public void validation(@Validated Items items, BindingResult bindingResult) {

  List<ObjectError> allErrors = bindingResult.getAllErrors();
  for (ObjectError allError : allErrors) {
    System.out.println(allError.getDefaultMessage());
  }
}
           

由于我在測試的時候,已經把日期轉換器關掉了,是以提示了字元串不能轉換成日期,但是名稱的校驗已經是出來了…

卧槽,原來SpringMVC可以這麼簡單

分組校驗其實就是為了我們的校驗更加靈活,有的時候,我們并不需要把我們目前配置的屬性都進行校驗,而需要的是目前的方法僅僅校驗某些的屬性。那麼此時,我們就可以用到分組校驗了…

步驟:

  • 定義分組的接口【主要是辨別】
  • 定于校驗規則屬于哪一各組
  • 在Controller方法中定義使用校驗分組
卧槽,原來SpringMVC可以這麼簡單
卧槽,原來SpringMVC可以這麼簡單
卧槽,原來SpringMVC可以這麼簡單

在我們之前SSH,使用Struts2的時候也配置過統一處理異常…

當時候是這麼幹的:

  • 在service層中自定義異常
  • 在action層也自定義異常
  • 對于Dao層的異常我們先不管【因為我們管不着,dao層的異常太緻命了】
  • service層抛出異常,Action把service層的異常接住,通過service抛出的異常來判斷是否讓請求通過
  • 如果不通過,那麼接着抛出Action異常
  • 在Struts的配置檔案中定義全局視圖,頁面顯示錯誤資訊

那麼我們這次的統一處理異常的方案是什麼呢????

我們知道Java中的異常可以分為兩類

  • 編譯時期異常
  • 運作期異常

對于運作期異常我們是無法掌控的,隻能通過代碼品質、在系統測試時詳細測試等排除運作時異常

而對于編譯時期的異常,我們可以在代碼手動處理異常可以try/catch捕獲,可以向上抛出。

我們可以換個思路,自定義一個子產品化的異常資訊,比如:商品類别的異常

public class CustomException extends Exception {
	
	//異常資訊
	private String message;
	
	public CustomException(String message){
		super(message);
		this.message = message;
		
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}
	
	

}
           

我們在檢視Spring源碼的時候發現:前端控制器DispatcherServlet在進行HandlerMapping、調用HandlerAdapter執行Handler過程中,如果遇到異常,在系統中自定義統一的異常處理器,寫系統自己的異常處理代碼。。

卧槽,原來SpringMVC可以這麼簡單
卧槽,原來SpringMVC可以這麼簡單

我們也可以學着點,定義一個統一的處理器類來處理異常…

public class CustomExceptionResolver implements HandlerExceptionResolver  {

	//前端控制器DispatcherServlet在進行HandlerMapping、調用HandlerAdapter執行Handler過程中,如果遇到異常就會執行此方法
	//handler最終要執行的Handler,它的真實身份是HandlerMethod
	//Exception ex就是接收到異常資訊
	@Override
	public ModelAndView resolveException(HttpServletRequest request,
			HttpServletResponse response, Object handler, Exception ex) {
		//輸出異常
		ex.printStackTrace();
		
		//統一異常處理代碼
		//針對系統自定義的CustomException異常,就可以直接從異常類中擷取異常資訊,将異常處理在錯誤頁面展示
		//異常資訊
		String message = null;
		CustomException customException = null;
		//如果ex是系統 自定義的異常,直接取出異常資訊
		if(ex instanceof CustomException){
			customException = (CustomException)ex;
		}else{
			//針對非CustomException異常,對這類重新構造成一個CustomException,異常資訊為“未知錯誤”
			customException = new CustomException("未知錯誤");
		}

		//錯誤 資訊
		message = customException.getMessage();
		
		request.setAttribute("message", message);

		
		try {
			//轉向到錯誤 頁面
			request.getRequestDispatcher("/WEB-INF/jsp/error.jsp").forward(request, response);
		} catch (ServletException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return new ModelAndView();
	}

}

           

<!-- 定義統一異常處理器 -->
	<bean class="cn.itcast.ssm.exception.CustomExceptionResolver"></bean>
           
卧槽,原來SpringMVC可以這麼簡單
卧槽,原來SpringMVC可以這麼簡單

我們在學習webservice的時候可能就聽過RESTful這麼一個名詞,當時候與SOAP進行對比的…那麼RESTful究竟是什麼東東呢???

RESTful(Representational State Transfer)軟體開發理念,RESTful對http進行非常好的诠釋。

如果一個架構支援RESTful,那麼就稱它為RESTful架構…

以下的文章供我們了解:

http://www.ruanyifeng.com/blog/2011/09/restful

綜合上面的解釋,我們總結一下什麼是RESTful架構:

  • (1)每一個URI代表一種資源;
  • (2)用戶端和伺服器之間,傳遞這種資源的某種表現層;
  • (3)用戶端通過四個HTTP動詞,對伺服器端資源進行操作,實作"表現層狀态轉化"。

簡單來說,如果對象在請求的過程中會發生變化(以Java為例子,屬性被修改了),那麼此是非幂等的。多次重複請求,結果還是不變的話,那麼就是幂等的。

PUT用于幂等請求,是以在更新的時候把所有的屬性都寫完整,那麼多次請求後,我們其他屬性是不會變的

在上邊的文章中,幂等被翻譯成“狀态統一性”。這就更好地了解了。

其實一般的架構并不能完全支援RESTful的,是以,隻要我們的系統支援RESTful的某些功能,我們一般就稱作為支援RESTful架構…

非RESTful的http的url:http://localhost:8080/items/editItems.action?id=1&…

RESTful的url是簡潔的:http:// localhost:8080/items/editItems/1

從上面我們可以發現,url并沒有.action字尾的,是以我們要修改核心配置設定器的配置

<!-- restful的配置 -->
	<servlet>
		<servlet-name>springmvc_rest</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<!-- 加載springmvc配置 -->
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<!-- 配置檔案的位址 如果不配置contextConfigLocation, 預設查找的配置檔案名稱classpath下的:servlet名稱+"-serlvet.xml"即:springmvc-serlvet.xml -->
			<param-value>classpath:spring/springmvc.xml</param-value>
		</init-param>

	</servlet>
	<servlet-mapping>
		<servlet-name>springmvc_rest</servlet-name>
		<!-- rest方式配置為/ -->
		<url-pattern>/</url-pattern>
	</servlet-mapping>
           

在Controller上使用PathVariable注解來綁定對應的參數

//根據商品id檢視商品資訊rest接口
	//@RequestMapping中指定restful方式的url中的參數,參數需要用{}包起來
	//@PathVariable将url中的{}包起參數和形參進行綁定
	@RequestMapping("/viewItems/{id}")
	public @ResponseBody ItemsCustom viewItems(@PathVariable("id") Integer id) throws Exception{
		//調用 service查詢商品資訊
		ItemsCustom itemsCustom = itemsService.findItemsById(id);
		
		return itemsCustom;
		
	}

           

當DispatcherServlet攔截/開頭的所有請求,對靜态資源的通路就報錯:我們需要配置對靜态資源的解析

<!-- 靜态資源 解析 -->
	<mvc:resources location="/js/" mapping="/js/**" />
	<mvc:resources location="/img/" mapping="/img/**" />

           

/**

就表示不管有多少層,都對其進行解析,

/*

代表的是目前層的所有資源…

在Struts2中攔截器就是我們當時的核心,原來在SpringMVC中也是有攔截器的

使用者請求到DispatherServlet中,DispatherServlet調用HandlerMapping查找Handler,HandlerMapping傳回一個攔截的鍊兒(多個攔截),springmvc中的攔截器是通過HandlerMapping發起的。

實作攔截器的接口:

public class HandlerInterceptor1 implements HandlerInterceptor {

	//在執行handler之前來執行的
	//用于使用者認證校驗、使用者權限校驗
	@Override
	public boolean preHandle(HttpServletRequest request,
			HttpServletResponse response, Object handler) throws Exception {
		
		System.out.println("HandlerInterceptor1...preHandle");
		
		//如果傳回false表示攔截不繼續執行handler,如果傳回true表示放行
		return false;
	}
	//在執行handler傳回modelAndView之前來執行
	//如果需要向頁面提供一些公用 的資料或配置一些視圖資訊,使用此方法實作 從modelAndView入手
	@Override
	public void postHandle(HttpServletRequest request,
			HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		System.out.println("HandlerInterceptor1...postHandle");
		
	}
	//執行handler之後執行此方法
	//作系統 統一異常處理,進行方法執行性能監控,在preHandle中設定一個時間點,在afterCompletion設定一個時間,兩個時間點的差就是執行時長
	//實作 系統 統一日志記錄
	@Override
	public void afterCompletion(HttpServletRequest request,
			HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
		System.out.println("HandlerInterceptor1...afterCompletion");
	}

}
           

配置攔截器

<!--攔截器 -->
	<mvc:interceptors>
		<!--多個攔截器,順序執行 -->
		<!-- <mvc:interceptor>
			<mvc:mapping path="/**" />
			<bean class="cn.itcast.ssm.controller.interceptor.HandlerInterceptor1"></bean>
		</mvc:interceptor>
		<mvc:interceptor>
			<mvc:mapping path="/**" />
			<bean class="cn.itcast.ssm.controller.interceptor.HandlerInterceptor2"></bean>
		</mvc:interceptor> -->
		
		<mvc:interceptor>
			<!-- /**可以攔截路徑不管多少層 -->
			<mvc:mapping path="/**" />
			<bean class="cn.itcast.ssm.controller.interceptor.LoginInterceptor"></bean>
		</mvc:interceptor>
	</mvc:interceptors>
           

如果兩個攔截器都放行

測試結果:
HandlerInterceptor1...preHandle
HandlerInterceptor2...preHandle

HandlerInterceptor2...postHandle
HandlerInterceptor1...postHandle

HandlerInterceptor2...afterCompletion
HandlerInterceptor1...afterCompletion

總結:
執行preHandle是順序執行。
執行postHandle、afterCompletion是倒序執行
           

1 号放行和2号不放行

測試結果:
HandlerInterceptor1...preHandle
HandlerInterceptor2...preHandle
HandlerInterceptor1...afterCompletion

總結:
如果preHandle不放行,postHandle、afterCompletion都不執行。
隻要有一個攔截器不放行,controller不能執行完成

           

1 号不放行和2号不放行

測試結果:
HandlerInterceptor1...preHandle
總結:
隻有前邊的攔截器preHandle方法放行,下邊的攔截器的preHandle才執行。
           

日志攔截器或異常攔截器要求

  • 将日志攔截器或異常攔截器放在攔截器鍊兒中第一個位置,且preHandle方法放行

攔截器攔截

public class LoginInterceptor implements HandlerInterceptor {

	//在執行handler之前來執行的
	//用于使用者認證校驗、使用者權限校驗
	@Override
	public boolean preHandle(HttpServletRequest request,
			HttpServletResponse response, Object handler) throws Exception {
		
		//得到請求的url
		String url = request.getRequestURI();
		
		//判斷是否是公開 位址
		//實際開發中需要公開 位址配置在配置檔案中
		//...
		if(url.indexOf("login.action")>=0){
			//如果是公開 位址則放行
			return true;
		}
		
		//判斷使用者身份在session中是否存在
		HttpSession session = request.getSession();
		String usercode = (String) session.getAttribute("usercode");
		//如果使用者身份在session中存在放行
		if(usercode!=null){
			return true;
		}
		//執行到這裡攔截,跳轉到登陸頁面,使用者進行身份認證
		request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response);
		
		//如果傳回false表示攔截不繼續執行handler,如果傳回true表示放行
		return false;
	}
	//在執行handler傳回modelAndView之前來執行
	//如果需要向頁面提供一些公用 的資料或配置一些視圖資訊,使用此方法實作 從modelAndView入手
	@Override
	public void postHandle(HttpServletRequest request,
			HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		System.out.println("HandlerInterceptor1...postHandle");
		
	}
	//執行handler之後執行此方法
	//作系統 統一異常處理,進行方法執行性能監控,在preHandle中設定一個時間點,在afterCompletion設定一個時間,兩個時間點的差就是執行時長
	//實作 系統 統一日志記錄
	@Override
	public void afterCompletion(HttpServletRequest request,
			HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
		System.out.println("HandlerInterceptor1...afterCompletion");
	}

}
           

Controller

@Controller
public class LoginController {
	
	
	//使用者登陸送出方法
	@RequestMapping("/login")
	public String login(HttpSession session, String usercode,String password)throws Exception{
		
		//調用service校驗使用者賬号和密碼的正确性
		//..
		
		//如果service校驗通過,将使用者身份記錄到session
		session.setAttribute("usercode", usercode);
		//重定向到商品查詢頁面
		return "redirect:/items/queryItems.action";
	}
	
	//使用者退出
	@RequestMapping("/logout")
	public String logout(HttpSession session)throws Exception{
		
		//session失效
		session.invalidate();
		//重定向到商品查詢頁面
		return "redirect:/items/queryItems.action";
		
	}
	

}

           

  • 使用Spring的校驗方式就是将要校驗的屬性前邊加上注解聲明。
  • 在Controller中的方法參數上加上@Validation注解。那麼SpringMVC内部就會幫我們對其進行處理(建立對應的bean,加載配置檔案)
  • BindingResult可以拿到我們校驗錯誤的提示
  • 分組校驗就是将讓我們的校驗更加靈活:某方法需要校驗這個屬性,而某方法不用校驗該屬性。我們就可以使用分組校驗了。
  • 對于處理異常,SpringMVC是用一個統一的異常處理器類的。實作了HandlerExceptionResolver接口。
  • 對子產品細分多個異常類,都交由我們的統一異常處理器類進行處理。
  • 對于RESTful規範,我們可以使用SpringMVC簡單地支援的。将SpringMVC的攔截.action改成是任意的。同時,如果是靜态的資源檔案,我們應該設定不攔截。
  • 對于url上的參數,我們可以使用@PathVariable将url中的{}包起參數和形參進行綁定
  • SpringMVC的攔截器和Struts2的攔截器差不多。不過SpringMVC的攔截器配置起來比Struts2的要簡單。
    • 至于他們的攔截器鍊的調用順序,和Filter的是沒有差别的。
卧槽,原來SpringMVC可以這麼簡單