SpringMVC学习记录

本文详细介绍SpringMVC框架的配置及使用方法,包括环境搭建、请求处理、数据绑定、视图解析等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 SpringMVC是目前主流的MVC框架之一,支持REST风格的URL请求,文首先将Spring4.3的官方文档放出来,有需要自己研究。

1. SpringMVC的HelloWorld

 创建SpringMVC的步骤主要分为下面的几个:

  • 加入jar包;
  • 在web.xml中配置DispatcherServlet
  • 加入SpringMVC的配置文件
  • 编写请求处理器,并标识为处理器
  • 编写视图

好了,按照以上的步骤开始来搞:
1.首先利用IDEA创建好WebApplication的Module后,导入必须的jar:commons-logging、spring-aop、spring-bean、spring-context、spring-core、spring-expression、spring-web、spring-webmvc,然后在Project Structurez(Ctrl + Shift + Alt + S)关联Spring框架并导入上述的jar包,添加web.xml文件(注意将位置改到/web/WEB-INF/下);

2.配置web.xml,主要是配置SPringMVC的核心Servlet,配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
          http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
           version="3.0">
    <!--配置DispatcherServlet-->
    <servlet>
        <servlet-name>springDispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--配置DispatcherServlet的一个初始化参数,这里是指SpringMVC配置文件的名称和位置
            实际上也可以不使用contextConfigLocation来配置SpringMVC的配置文件,直接使用默认的,
            默认的SpringMVC配置文件为:/WEB-INF/springDispatcherServlet-servlet.xml,此时就不需要
            在对SpringMVC的配置文件的名字和位置进行配置了
        -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <!--表明这个Servlet在应用被加载的时候就被创建,而不是等第一次被请求才创建-->
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>springDispatcherServlet</servlet-name>
        <!--拦截所有请求-->
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

3.然后在src目录下加入springMVC的配置文件:springmvc.xml,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!--配置自动扫描的包-->
    <context:component-scan base-package="com.hhu"/>

    <!--配置视图解析器,其实就是解析Controller中的方法返回值解析成实际的物理试图-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--配置视图的前缀和后缀-->
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

4.编写处理器,主要是标识控制器和URL的映射路径,主要代码如下:

@Controller
public class HelloWorld {

    /*
    使用@RequestMapping来映射URL请求,通过访问/helloWorld这个路径即可访问这个方法,做完逻辑处理后,最终返回对应的视图
     */
    @RequestMapping("/helloWorld")
    public String hello() {
        System.out.println("Hello World!");
        /*对于Controller中方法的返回值,视图解析器会将它解析为实际的物理试图,
        对于InternalResourceViewResolver视图解析器会做如下的解析:
        prefix + returnValue + suffix,然后得到实际的物理视图再转发
         */
        return "success";
    }
}

5.编写视图,下面写了一个简单的视图:

 <!--默认位置的index.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>$Title$</title>
  </head>
  <body>
      <a href="helloSpringMVC">Hello SpringMVC!</a>
      <a href="helloWorld">Hello!</a>
  </body>
</html>

<!--在web的应用目录中的WEB-INF下创建views文件夹并在其中创建success.jsp,
这个里面随便写点什么已做区分-->

通过上面的几个步骤,在IDEA中配置案例Application context的后即可访问上述主页index,点击超链接访问Application的helloWorld的方法。

2.关于注解@RequestMapping

 通过上面的小栗子可以知道在SpringMVC中使用@RequestMapping这个注解来映射URL地址,为控制器指定可以处理哪些请求,这个注解可以放在控制器的类定义和方法定义上:

  • 类定义上:这里的类定义就是指的控制器的类定义,提供WEB Application的初步请求应映射信息。这个URL是相对于WEB应用的根目录。
  • 方法定义上:提供进一步的细分映射信息,相对与于类定义的URL,若类定义处没有加@RequestMapping注解。同样的方法上的@RequestMapping注解的URL也是相对于WEB应用的根目录。

这里拿上面的栗子再细说一下,关于类定义上加@RequestMapping,比如在上面的HelloWorld类上除了@Controller注解外再加上@RequestMapping注解,如下:

@RequestMapping("/HelloWorldController")
@Controller
public class HelloWorld {
    //...
}

那么此时再想访问控制器下hello方法映射的URL时就不再是ApplicationContext/helloWorld这个路径了,而是ApplicationContext/HelloWorldController/helloWorld,还有记住不要忘了ApplicationContex,这个在Eclipse中会自动以项目名创建的,但是在IDEA中默认为空,需要自己手动创建。

 下面详细的来看一下@RequestMapping中的一些属性以及说明,先看一下标准的HTTP请求报头:
这里写图片描述
里面包含了请求方法、请求的URL、HTTP协议和版本、报文头、报文体,在@RequestMapping注解中同样的可以定义这些属性,其中value对应的是请求的URL(如果该注解中没有定义其他的属性,那么默认可以不写),method对应于请求方法,params对应于请求参数,heads对应于请求头。下面具体的来看它们的用法:

 第一点,请求方法属性method,常用的请求方式有get(缺省时默认此方式)和post,比如下面的定义方式:

@RequestMapping(value = "/post", method = RequestMethod.POST)
public String Post() {
    System.out.println("come to post");
    return "success";
}

【注意】在测试的时候,需要写一个form表单才能提交post请求,或者就直接不写了用postman在做测试即可。

 第二点,请求参数params和请求头heads支持一些简单的表达式:

  • param1:表示请求必须包含名为param1的请求参数;
  • !param1:表示请求不能包含名为param1的请求参数;
  • param1 != value1:表示请求包含名为param1的请求参数,但是它的值不能为value1;
  • {"param1=value1", "param2"}:表示请求必须包含名为param1param2的两个参数,且param1的参数必须为value1。

对于上面的方式有如下的小栗子:

@RequestMapping(value = "/testParams", params = {"name", "age!=10"}, , headers = {"Accept-Language=zh-CN,en-US;q=0.5"})
public String testParams() {
   System.out.println("come to Params9");
   return "success";
}

利用postman测试的时候必须有nameage两个参数,且age的值不能为10,在测试的时候,age为其他值的时候提交请求的时候会报404的错误,而且报头必须包含Accept-Language参数且值为zh-CN,en-US;q=0.5,可以测试。

 此外,除了上面的所说的几点,@RequestMapping注解中的value属性值还支持通配符的表示方式来配,但是就必须是Ant风格的通配符:匹配一个字符用?,匹配任意字符用*,匹配多层路径用**,比如下面的几个含有通配符的URL:

  • /user/*/hello:可以匹配/user/a/hello也可以匹配/user/bb/hello等;
  • /user/**/hello:可以匹配多层路径,比如/user/hello也可以匹配/user/aa/hello,还可以匹配/user/aa/bbb/hello等;
  • /user/hello?:可以匹配/user/helloh或者/user/hellop等,当然含有多个问号可以匹配多个对应个数的字符;

3.关于控制器中处理方法的签名参数的各种注解

@PathVariable注解是用来绑定映射URL中的占位符,通常用在方法的参数上。在上面使用@RequestMapping注解中的url属性设置URL映射路径的时候,URL中可以使用{参数名}的方式做占位符,比如/user/{id}/hello使用{id}做URL中的占位符,在写这个方法的时候就可以使用@PathVariable这个注解将id绑定到对应的参数上,比如:

@RequestMapping("/pathVar/{id}")
public String testPath(@PathVariable("id") int id) {
    System.out.println("id为: " + id);
    return "success";
}

在访问ApplicationContext/pathVar/3的时候,控制器就可以获取id的值,注意如果在访问上述的URL时,id路径参数必须要有,不带上id这路径的话,会报404。

@RequestParam注解可以绑定请求参数(通过前端URL中传入的参数将其绑定到后端控制器的对应处理方法中的签名上),语法:

@RequestParam(value = "参数名", required = false) String 参数名

其中required = true/false属性是非必须的,但是默认是true,就是在输入URL时必须带上这个参数,否则报错,当然也可以设置为false,在访问时对于该参数可带可不带,比如:

@RequestMapping("/testParam")
public String testParam(@RequestParam(value = "userName", required = true) String userName) {
    System.out.println("userName:" + userName);
    return "success";
}

在访问ApplicationContext/SpringMVC/testParam?userName=jack时,后端控制器会自动获取URL中的userName的值,在后端控制器中输出userName:jack,这个方法和前种@PathVariable注解有些类似,但是它带的参数可设置是否必须带上,如果是将参数绑定到int类型的参数上,注意为空的情况会映射报错,所以在转成int型时,如果不想这种情况发生,需要添加默认值,比如:

@RequestParam(value = "age", required = true, defaultValue = 0)

@RequestHeader注解用于绑定请求报头的属性,请求头中包含了若干个属性,服务器可以根据它获取客户端的信息,通过@RequestHeader即可将请求头中的属性值绑定到处理方法的签名(即传入参数)上,比如:

@RequestMapping(value = "/testHeader")
public String testHeader(@RequestHeader(value = "Accept-Encoding") String encode) {
    System.out.println("encoding:" + encode);
    return "success";
}

在客户端访问ApplicationContext/testHeader时,后端控制器中可以获取请求头中相应的信息。

@CookieValue可以获取某个Cookie的值,这里需要传入Cookie的name,然后直接根据Cookie的name来过去他们各自的value,比如下面:

    @RequestMapping("/testCookie")
    public String testCookie(@CookieValue("JSESSIONID") String sessionID) {
        System.out.println("CookieValue:" + sessionID);
        return "success";
    }

至于这里的Cookie的name需要在浏览器工具中查看。

 除了上面几种绑定参数的方法外,还有使用POJO对象绑定请求参数值的方式(因为如果要映射一个对象,用上述的方法需要映射的变量会很多,不是很方便)SpringMVC中,请求参数可以使用POJO对象进行映射,SpringMVC会自动按照请求参数名和POJ对象属性进行自动匹配,自动为该对象填充属性,而且还支持级联属性。在映射POJO时不需要注解,使用频率较高,比如下面提交表单的场景:

//定义两个实体类User,和Address类进行关联
public class User {
    private String userName;
    private String email;
    private int age;
    private String password;
    private Address address;
    //提供getter和setter方法
}

public class Address {
    private String city;
    private String street;
    //提供getter和setter方法
}

//控制器中绑定对象
@RequestMapping("/testPOJO")
public String testPojo(User user) {
    System.out.println(user);
    return "success";
}

提交的表单:

<form action="testPOJO" method="post">
    userName:<input type="text" name="userName"/><br>
    email:<input type="text" name="email"/><br>
    age:<input type="text" name="age"/><br>
    password:<input type="password" name="password"/><br>
    city:<input type="text" name="address.city"/><br>
    street:<input type="text" name="address.street"/><br>
    <input type="submit" value="submit"/>
</form>

在提交后,控制器自动会自动解析提交的信息,然后自动填充到User的各个属性中。

4.关于REST

 REST即Representational State Transfer,资源表现层状态转化,是当前比较流行的互联网架构。关于上面的定义中有资源、表现层和状态转化一些关键词:

  • 资源:网络上的一个实体,或者说是网络上的一个具体信息,它可以是一段文本、一张图片等具体存在,可以用RUI(统一资源定位符),每种资源对应一个特定的URI,想获取一个资源只要获取它的URI即可,因为URI是每个资源独一无二的识别符。

  • 表现层(Representation):把资源的具体表现出来的形式就叫表现(Representation)。比如文本可以用txt格式表示,也可以用HTML格式、XML、JSON等格式表示。

  • 状态转化(State Transfer):每发出一个请求,就代表客户端和服务器的一个交互过程。HTTP协议,是一个无状态协议,即所有状态都保存在服务器端,因此,如果客户端像操作服务器,必须通过某种的手段让服务器的状态发生改变,这种状态的转换是建立在表现层上,所以就是“状态转化”,具体说就是HTTP协议里面的四个操作行为的动词:GET、POST、PUT、DELETE,它们分别对应四种基本操作:其中前两种请求方式是比较常见的方式,GET是获取资源,POST用于新增资源,PUT用于更新资源,DELETE用来删除资源。

【注意】上面的四种请求方式刚好和数据库的增删改查想对应,如果用REST来搞一个订单的增删改查的请求URL就是下面的几个:
- /order/1 GET方式:得到id为1的订单;
- /order/1 DELETE方式:删除id为1的订单;
- /order/1 PUT方式:更新id为1的订单;
- /order POST方式:新增订单;
这里面浏览器from表单只支持GET和POST请求,而DELETE和PUT等方式并不支持,Spring3.0添加了过滤器HiddenHttpMethodFilter,这个过滤器可以将这写请求转换成标准的HTTP请求,使得支持GET、POST、PUT、DELETE请求。

对于上面的几种请求,一般而言,默认都是GET请求,而from表单提交一般可以选get或post请求,在SpringMVC的Controller中请求方式的配置正常即可,而对于PUTDELETE请求而言必须从POST请求转变成对应的请求,需要两点:

  • 在web.xml文件中配置转化使用的过滤器HiddenHttpMethodFilter,详细配置如下:
    <!--配置HiddenHttpMethodFilter,这个Filter可以将POST请求转成UPDATE或者DELETE-->
    <filter>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
  • 在前端页面中必须设置为POST请求,并且添加name="_method"的隐藏域,注意必须为_method(这个是过滤器里面的名字,不能写其他),主要如下:
    <form action="testRestDELETE/1" method="post">
        <!--这个隐藏域不能丢,是什么方式,value就写什么-->
        <input type="hidden" name="_method" value="DELETE"/>
        <input type="submit" value="TestRestDELETE"/>
    </form>
  • 后端控制器中在定义@RequestMappingmethod属性时需要写上对应的方法(注意是自己的方法,而不是POST),比如:
    @RequestMapping(value = "/testRestDELETE/{id}", method = RequestMethod.DELETE)
    public String testRestDELETE(@PathVariable("id") int id) {
        System.out.println("come to RestDELETE--id:" + id);
        return "success";
    }

5.SpringMVC支持的原生ServletAPI

 在SpringMVC的控制器中的方法中,可以接受的原生ServletAPI有如下:

  • HttpServletRequest
  • HttpServletResponse
  • HttpSession
  • java.security.Principal
  • Locale
  • InputStream
  • OutputStream
  • Reader
  • Writer
    也就是说以上类型的参数可以作为控制器中任意方法的参数传入。

6.SpringMVC对模型数据的处理

 在SpringMVC中,提供了以下的四种方式输出模型数据:

  • ModelAndView:处理方法返回值类型为ModelAndView时,其中既包含视图也包含模型数据信息,方法即通过该对象添加模型视图数据,比如下面的小栗子:
@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView() {
    //设置模型视图中的视图数据,返回success.jsp视图
    String view = "success";
    ModelAndView modelAndView = new ModelAndView(view);

    //添加模型数据到模型视图中,SpringMVC会将ModelAndView对象存到request的域对象中
    modelAndView.addObject("time", new Date());

    return modelAndView;
}

 然后前端的success.jsp页面可以用如下的方式来获取ModelAndView中的模型数:

time:${requestScope.time}
  • Map和Model:传入参数为org.springframework.ui.Model或者org.springframework.ui.ModelMap或者java.util.Map时,处理方法返回时,Map中的数据会自动添加到模型中,比如下面的小栗子:
@RequestMapping("/testMapping")
public String testMap(Map<String, Object> map) {
    map.put("names", Arrays.asList("Tom","Jerry","Mike"));
    return "success";
}

 然后在前端页面中获取和ModelAndView中的获取方式一致:

names:${requestScope.names}

 就可以在页面上显示Tom,Jerry,Mike

  • @SessionAttributes注解:将模型中的某个属性暂存到HttpSession中,以便多个请求之间可以共享这个属性(这个注解是放在控制器类上,而不是方法上),在SpringMVC中除了可以通过属性名指定需要放到哪个会话的属性中,还可以通过模型属性的对象类型指定哪些模型属性需放到会话中(那么指定类型的参数全部都会放到Session中),如果要存放类型到Session中,在该注解中添加types = String.class属性,那么所有的String类型都会被存放到Session中,比如:
//将user放到Session中
@SessionAttributes("user")
@Controller
public class HelloWorld {
    @RequestMapping("/testSessionAttributes")
    public String testSessionAttributes(Map<String, Object> map) {
        User user = new User("Tom", "tom@163.com", 20);
        //将数据放到map中,再将map放到请求域中request
        map.put("user", user);
        return "success";
    }
}

 那么在前端页面上可以从Session域中获取这个属性:

sessionUser:${sessionScope.user}
  • @ModelAttribute注解:这个注解可以修饰方法(修饰方法时该方法就不会再做URL的映射),也可以修饰POJO类型(即对象型,注意一定是对象型)的传入参数。方法传入参数该注解中后,传入参数的对象就会放到数据模型中,注意@ModelAttribute注解修饰的方法中,放到Map时的键需要和目标方法的传入参数类型的第一个字符串一致。在SpringMVC中的运行流程如下:首先调用@ModelAttribute修饰的方法,将数据放到Map里,然后再把Map放到implicitiModel中,然后解析请求处理器的目标参数(默认是传入参数类型的首字母小写的形式,比如传入参数为User user,那么默认为"user",但是也可以使用@ModelAttribute去修饰取任意名称的变量),该目标参数来自于WebDataBinder对象的target属性。SpringMVC中对于使用该注解实现POJO的传入参数对应属性值的绑定过程相对比较复杂,在传入的对象参数时,首先在implictiModel中查找key对应的对象(就是确定传入参数的过程),若存在,则作为参数传入,若不存在,则检查方法所在的控制器Controller类上是否存在@SessionAttributes注解,若在Session中存在所需的key,则从Session中取出传入,如果不存在则抛出异常,如果控制器上根本就没有@SessionAttributes注解或者该注解中没有所需的key,那么就会通过反射来创建POJO类型的参数传入,最后SpringMVC会将key-value保存到implicitiModel中,近而保存到Request中。下面看个小栗子会清晰点吧:
    /**
     * 运行流程:
     * 1. 执行@ModelAttribute注解的方法,从数据库中取出对象放入Map中
     * 2. SpringMVC从Map中取出User,并把表单请求参数赋给User对应的属性
     * 3. SpringMVC把上述对象传入目标的参数
     */
    @ModelAttribute
    public void getUser(@RequestParam(value = "id", required = false) Integer id, Map<String, Object> map) {
        //这里是模拟从数据中取数据然后放入map的过程
        if(id!=null) {
            User user = new User(1, "jack", "jacksonary@163.com", 29, "1234");
            System.out.println("从数据库中获取一个对象:" + user);
            map.put("user", user);
        }
    }

    /*
    注意这里传入的参数使用了@ModelAttribute注解指明了所需的POJO的key为"user"
    所以直接去寻找key为"user"对应的pojo,当然如果写成@ModelAttribute("aa") User user
    这样的形式,那就是去寻找key为"aa"的pojo了;除此以外,这里的传入POJO对象也可以不用
    @ModelAttribute注解指定,默认是传入对象类名首字母小写,比如这里直接写成User user,那么
    它会直接去找key为"user"的pojo,如果是TestPOJO testPojo,那么他会去找key为"testPOJO"的
    对象。
     */
    @RequestMapping("/testModelAttribute")
    public String testModelAttribute(@ModelAttribute("user") User user) {
        System.out.println("修改后1:" + user);
        return "success";
    }

然后前端页面代码为:

<form action="testModelAttribute" method="post">
    <!--一般来说对象的id都是隐藏的-->
    <input name="id" type="hidden" value="1"/>
    userName:<input name="userName" type="text" value="jack"/><br>
    email&emsp;&nbsp;:<input name="email" type="text" value="jack@163.com"/><br>
    age&emsp;&emsp;&nbsp;:<input name="age" type="text" value="20"/><br>
    submit:<input type="submit" value="submit"/>
</form>

重要】上面的流程为:有个from表单,里面有姓名、邮箱、年龄(但是实际实体类定义的时候还有其他的属性,比如密码),填写表单后提交,这时SpringMVC会首先执行控制器中带有@ModelAttribute注解的方法(去数据库中取数据的过程),然后执行对应映射的URL的方法testModelAttribute,首先会将表单提交过来的pojo和完整的pojo相应的属性对应过来(应为提交过来的有姓名、邮箱、年龄,所以testModelAttribute方法中传入的User对象是有这些值的),但是他没有传入密码呀,这个时候所有剩余的pojo缺失的属性值全部由@ModelAttribute注解的方法中放入Map的pojo提供(这个方法中放入的map名字,即key值一定要和testModelAttribute传入参数的key值一致!!这里都是user,否则不能赋值),如果控制器中没有@ModelAttribute注解的方法,那么会再去@SessionAttributes注解中的查询对相应的key的对象,如果有对应的key却没有相应的value 就会抛出异常,如果压根没有这个注解那么会利用反射去创建pojo。整个过程就是这个样子,唉…好累(;´༎ຶД༎ຶ`),具体的流程走向需要参看SpringMVC的源码断点调式查看,有点难,大体的流程可以整明白,细节的源码我看起来还是太吃力了……

7. 视图和视图解析器

 在一开始的小节中就已经在配置SpringMVC的配置文件时涉及到关于视图解析器的配置,主要如下:

    <!--配置视图解析器,其实就是解析Controller中的方法返回值解析成实际的物理试图-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--前缀,注意最后的斜杠不要丢!-->
        <property name="prefix" value="/WEB-INF/views/"/>
        <!--后缀-->
        <property name="suffix" value=".jsp"/>
    </bean>

就是这么简单,在第6小节中,记录模型数据的处理方式时,可以发现在控制器中,方法的返回值绝大部分情况下都是会落实到视图上,这里注意不管是返回的String还是ModelAndView亦或者是View,只要它们的方法上加了@RequestMapping去映射各自的URL时,那么SpringMVC最后都会给转成ModelAndView类型;
 需要注意的是,上面配置的视图解析器为InternalResourceViewResolver(这也是默认的视图解析器),但是如果项目中使用了JSTL,那么SpringMVC会自动将上面配置好的视图解析器转为JstlView,如果使用JSTL的fmt标签必须要在SpringMVC配置文件中配置国际化资源文件,比如下面的小栗子(导入jstl所需要的jar–jstl-1.2.jar和standard-1.1.2.jar):

    <!--配置国际化资源文件-->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="i18n"/>
    </bean>

国际化配置文件:

# i18n_en_US.properties文件
i18n.username = Username
i18n.password = Password

# i18n_zh_CN.properties文件
i18n.username = 用户名
i18n.password = 密码

编写页面jsp

<!--注意引入fmt标签-->
<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<fmt:message key="i18n.username"/>
<br><br>
<fmt:message key="i18n.password"/>

在浏览器中展示此页面时,选择中文字符编码即会显示中文的配置文件,选择英文的编码即可显示英文的配置文件的内容。
 在一般情况下,除了主页index.jsp的其他jsp页面正常都是放在WEB-INF目录下的,而该目录下的页面是无法直接访问的,只能通过请求转发才能访问到该目录下的资源,但是如果是转发的话又必须经过控制器的方法才能转发到相应的请求,但是现在有一种需求,我不想经过控制器而可以直接访问该安全目录下的页面资源,看一下普通的转发方法:

@RequestMapping("/helloWorld")
public String hello() {
    System.out.println("Hello World!");
    return "success";
}

通过控制器如上的方法后,用户需要访问success.jsp页面(该页面在WEB-INF中的views文件夹下)时,只有通过访问ApplicationContext/hellWorld才能转发到该页面,但是现在用户想直接通过AppllicationContext/success的方式访问到该页面,不需要再通过上面的hello方法,这个时候需要在SpringMVC的配置文件中配置<mvc:view-controller>标签:

<mvc:view-controller path="/success" view-name="success"/>

【注意】在配置完上述东西后,发现通过AppllicationContext/success是可以直接访问到success页面了,但是发现再访问ApplicationContext/hellWorld时页面直接报错404,而且不仅仅是上述的/helloWorld一个映射路径,控制器中所有映射的URL全部失效404。一般情况下上面<mvc:view-controller>标签都是和<mvc:annotation-driven>标签组合使用,这样就不会出现上面404的报错了,具体如下:

    <!--
        配置直接进入需要转发的页面,可以直接到相应的页面,无需经过Handler的方法再进行转发
        但是在配置这个之后,原先Controller中的映射的URL全部失效,访问报错404,所以配置
        这个的时候一般都会和标签mvc:annotation-driven配合使用,场景:我想直接进入之前
        需要转发才能进入的页面,不想经过控制器方法的转发。
    -->
    <mvc:view-controller path="/success" view-name="success"/>

    <!--在实际开发中都需要配置mv:annotation-driven标签-->
    <mvc:annotation-driven></mvc:annotation-driven>

 除了上面通常自带的视图外,我们可以通过实现org.springframework.web.servlet.View接口来自定义视图(这个视图必须有IOC容器管理),然后通过在SpringMVC配置文件中除了InternalResourceViewResolver视图解析器,还需配置一个根据视图名解析视图的视图解析器BeanNameViewResolver,最后控制器中方法直接返回视图名字的Bean的名字(Spring默认类名首字母小写)。好了,结合上述的流程,看一个小栗子:
自定义的视图HelloView

//一定要加上Spring的管理注解,后面IOC容器才能根据首字母小写的类名从里面获取视图
@Component
public class HelloView implements View {
    @Override
    public String getContentType() {
        return "text/html";
    }

    @Override
    public void render(Map<String, ?> map, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
        httpServletResponse.getWriter().print("hello view, time: " + new Date());
    }
}

SpringMVC配置文件中配置视图解析器:

    <!--配置BeanNameViewResolver视图解析器,使用视图的名字来解析视图-->
    <bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
        <!--通过order属性定义视图解析器的优先级,值越小优先级越高-->
        <property name="order" value="100"></property>
    </bean>

控制器中返回自定义视图:

    @RequestMapping("/testView")
    public String testView() {
        System.out.println("testView");
        return "helloView";
    }

最后访问ApplicationContext/testView页面显示:hello view, time: Tue Mar 13 09:07:59 CST 2018,通常自定义视图用于整合EXCEL以及JFreeChart等一些组件。
 在上面所有的小栗子都是通过控制器实现转发(URL不改变)访问对应的资源,当然也可以实现重定向,在SpringMVC中,如果返回的字符串带有forward:或者redirect:前缀时,将会做特殊的处理:

  • forward:success:完成到success.jsp页面的转发操作;
  • redirect:success:完成到success.jsp页面的重定向操作。

8. 关于SpringMVC中对于静态资源的处理

 在SpringMVC中,由于配置了DispatcherServlet拦截器,它是拦截了所有的请求,所以只要在web项目根目录下(在IDEA中WEB项目的根目录默认为web,Eclipse中默认为webapp,各有不同)的资源都必须经过映射才能访问到(包括静态资源!),所以常常都会有引入jquery、css、js等静态资源没法找到,报404的错误,这个错误往往是令人烦躁的一个问题,在处理静态资源时需要在SpringMVC的配置文件中配置默认的handler,主要如下:

<!--配置静态资源的访问不到的问题
    解决问题的原理:配置默认的Handler(即Tomcat中默认的Servlet),如果资源没有在控制器中配置映射的路径,
    默认的Handler会帮我们自动去寻找对应的资源,这里主要是指静态资源
-->
<mvc:default-servlet-handler/>

<!--默认的Handler必须配合annotation-driven使用,否则可以访问静态的同时会导致控制器中的URL无法访问导致404错误-->
<mvc:annotation-driven/>

9.关于定义转换器

 在SpringMVC中内部通过转换器来进行后台对象参数到前端页面参数的绑定,内部有一个默认的对象转换器,这里我们同样可以定义自己需要的转换器,而且在配置自定义的转换器后,SpringMVC中默认的转换器仍然生效,下面通过一个小栗子看一下自定义转换器的配置流程:
* 首先在页面创建转换器的格式需求,这个有点像是需求分析了,搞明白需要转换的是什么,下面是页面需求:

<!--测试将Employee转成String类型-->
<form action="testConv" method="POST">
    <!--lastName-email-gender-department.id-->
    Employee:<input type="text" name="employee">
    <input type="submit" value="Submit" />
</form>

 上面页面明确提出了需要转换的东西是一个形如“lastName-email-gender-department.id”的字符串;

  • 通过上面的表单提交动作可以虚拟一个保存到数据库的行为,那么很明显需要将这个String转换成一个对象Employee,上面知道了转换什么,现在知道了转换成什么,于是这里就需要去自定义转换器去实现了,自定义转换器需要实现org.springframework.core.convert.converter.Converter接口,重写convert方法,主要类定义如下:
//注意这个转换器需要放在IOC容器中
@Component
public class EmployeeConv implements Converter<String, Employee> {

    @Override
    public Employee convert(String s) {
        System.out.println("Enter Conversion");
        if (s != null) {
            String[] vals = s.split("-");
            if(vals!=null&&vals.length==4) {
                String lastName = vals[0];
                String email = vals[1];
                Integer gender = Integer.parseInt(vals[2]);
                Department department = new Department();
                department.setId(Integer.parseInt(vals[3]));

                Employee employee = new Employee(null, lastName, email, gender, department);
                System.out.println(s + "--convert--" + employee);
                return employee;
            }
        }
        return null;
    }
}

 除了上述的转换器的定义外,还需要在SpringMVC的配置文件中将其配置进去,主要做如下配置:

<!--
    两步:
        1. 配置ConversionServiceFactoryBean的Bean,注意里面
        2. 将第一步中配置的ConversionServiceFactoryBean配置到原来的annotation-driven
-->
<!--默认的Handler必须配合annotation-driven使用,否则可以访问静态的同时会导致控制器中的URL无法访问404-->
<mvc:annotation-driven conversion-service="conversionService"/>

<!--配置ConversionService,自定义的转换器,这个需要注入到annotation-driven中-->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <!--注意类名的首字母小写-->
            <ref bean="employeeConv"/>
        </set>
    </property>
</bean>
  • 经过上述的配置后,在前端页面提交的URL的控制器中的方法进行逻辑处理,在里面已经将字符串转换成了Employee对象,在Controller中可以直接进行操作:
    @RequestMapping("/testConv")
    public String testConv(@RequestParam("employee") Employee employee) {
        System.out.println("save: " + employee);
        employeeDao.save(employee);
        return "redirect:/emps";
    }

10.关于<mvc:annotation-driven>注解

 在前面的几个小节中,有三次用到了这个东西,第一次是在第7小节中想要不通过控制器转发而直接对jsp页面进行访问,在配置<mvc:view-controller>标签时,为了使原来控制器中映射的URL不失效,所以必须配置<mvc:annotation-driven>;还有一次是在第8小节处理静态资源无法访问的情况,使用<mvc:default-servlet-handler/>配置了默认的Handler,但是也导致了原来控制器中映射的URL生效,此时也需要配置<mvc:annotation-driven>才能解决问题;第三次是在配置自定义转换器时需要将配置好的conversionService放入到<mvc:annotation-driven>conversion-service属性中。

11. 关于数据格式转换

 前端页面往往会含有日期以及数值等类型的数据,那么在后台定义实体类的属性时需要做相应的处理:
* 日期类型:

@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birth;

 需要在属性定义的时候加上如上的注解,当然可以是任意合法的注解,然后在SpringMVC的配置文件中加上<mvc:annotation-driven/>即可,那么前端页面对应的birth属性的那一栏就可以yyyy-MM-dd形式填写,SpringMVC可以自动解析。

  • 数值类型,比如:
@NumberFormat(pattern = "#,###,###.#")
private Float salary;

 那么在前端页面填写salary对应的栏目时就可以以#,###,###.#形式填写,比如1,555,777.2,注意用#表示数值,同样需要在SpringMVC的配置文件中加入<mvc:annotation-driven>标签。

12. 关于JSON数据处理

 在当前json用的很多,在SpringMVC同样是这样,很简单,这里搞一个小栗子:

<script>
    $(function () {
        $("testJson").click(function () {
            var url = this.href;
            var args = {};
            $.post(url, args, function (data) {
                for(var i=0; i<data.length;i++) {
                    var id = data[i].id;
                    var lastName = data[i].lastName;
                    alert(id + ": " + lastName);
                }
            })
        })
    })
</script>

请求的URL在控制器中为:

@ResponseBody
@RequestMapping("/testJson")
public Collection testJson() {
    return employeeDao.getAll();
}

【注意】需要添加@ResponseBody注解即可处理JSON格式的数据,另外需加入jsckson要jar包,好像是3个(jackson-annotations-2.9.4.jar、jackson-core-2.9.4.jar、jackson-databind-2.9.4.jar),经过上面的配置这样后台相应页面请求时返回的就是json格式的数据了。

13. 关于HttpMessageConverter<T>

 使用HttpMessageConverter<T>将请求信息转化并绑定到处理方法的入参中或者将相应结果转为对应类型的相应信息,Spring提供了两种方式:

  • 使用@RequestBody@ResponseBody注解进行标注,前者是修饰入参、后者修饰控制器方法;
  • 使用HttpEntity<T>ResponseEntity<T>作为处理方法的入参或者返回值。

 是的,我平时使用的比较多的@ResponseBody就是属于HttpMessageConverter<T>,瞄一眼Spring中处理HttpMessageConverter<T>的流程:
这里写图片描述
 当控制器中的方法使用上述的两种方式时,Spring首先根据请求头或响应头的Accept属性匹配的HttpMessageConverter,进而根据参数类型或者泛型类型的过滤得到匹配的HttpMessageConverter(每种数据类型都有对应的HttpMessageConverter,比如Byte[]对应于ByteArrayHttpMessageConverter,String对应于StringHttpMessageConverter等等),若找不对应的HttpMessageConverter将报错,下面以文件的上传和下载搞一个小栗子:

<!--上传文件的表单-->
<form action="testHttpMessageConverter" method="POST" enctype="multipart/form-data">
    File:<input type="file" name="file"/><br>
    Desc:<input type="text" name="desc" /><br>
    <input type="submit" value="Submit"/><br>
</form>

<!--下载文件的连接-->
<a href="testResponseEntity">Download</a>

控制器方法的处理:

@ResponseBody
@RequestMapping(value = "/testHttpMessageConverter", method = RequestMethod.POST)
public String testHttp(@RequestBody String body) throws UnsupportedEncodingException {
    //用String修饰body,Spring自动匹配到StringHttpMessageConverter将前端上传的文件内容转成String类型
    System.out.println(body);
    //返回的内容直接显示给前端页面,看上图中的下半部分
    return "hello! " + new Date();
}

@RequestMapping("/testResponseEntity")
public ResponseEntity<byte[]> testResponseEntity(HttpSession httpSession) throws IOException {
    byte[] body = null;
    ServletContext servletContext = httpSession.getServletContext();
    //注意在IDEA的Module的web目录下存放该文件
    InputStream is = servletContext.getResourceAsStream("/files/test.txt");
    body = new byte[is.available()];
    is.read(body);

    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.add("Content-Disposition", "attachment;filename=abc.txt");

    HttpStatus status = HttpStatus.OK;

    //返回给前端页面的内容
    ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(body, httpHeaders, status);
    return responseEntity;
}

通过上述的配置后即可完成文件的上传和下载功能。

14.关于国际化

 首先对国际化问题做如下的几点说明:

  • 在页面上将根据客户端浏览器设置的语言情况对是文本(不是内容)进行本地化处理;
  • 可以在bean中获取国际化资源文件修改Local对应的消息;
  • 可以通过超链接切换Local,而不再依赖于浏览器的语言设置(这里是指手动切换浏览器的语言类型);

 对应于上述提出的三个问题,有如下的解决方案:

  • 使用JSTL的fmt标签;
  • 在bean中引入ResourceBundleMessageSource的示例,使其对应getMessage方法即可;
  • 配置LocalResolverLocalChangeInterceptor

用之前的栗子再看一下上述的问题。

问题一 其实就是最基本的国际化

第一步,创建国际化配置文件i18n文件:i18n.propertiesi18n_zh_CN.propertiesi18n_en_US.properties,表示原始配置文件、中文配置文件和英文配置文件:

# 原始配置文件和英文配置文件的内容
i18n.user=User
i18n.password=Password

# 中文配置文件
i18n.user=用户名
i18n.password=密码

第二步,配置jsp页面,引入fmt国际化标签:

<!--首页:跳转页面index.jsp-->
<a href="i18n">I18n Page</a>

<!--这是其中一个页面i18n.jsp-->
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<body>
    <!--注意fmt标签的用法-->
    <fmt:message key="i18n.user"></fmt:message>
    <br><br>
    <a href="i18n2">I18n Page2</a>
    <br><br>
    <a href="i18n">I18n Page</a>
</body>

<!--配置第二个页面i18n2.jsp
    和i18n.jsp页面一样,将fmt标签里面的内容换成下面的内容
-->
<fmt:message key="i18n.password"></fmt:message>

第三步,配置SpringMVC的配置文件:

    <!--配置页面直接跳转,不需要经过Controller方便测试-->
    <mvc:view-controller path="/i18n" view-name="i18n"></mvc:view-controller>
    <mvc:view-controller path="/i18n2" view-name="i18n2"></mvc:view-controller>

    <mvc:annotation-driven/>

    <!-- 配置国际化资源文件 -->
    <bean id="messageSource"
          class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="i18n"></property>
    </bean>

访问页面,可以通过Internet选项中更改中英文查看国际化的效果。

问题二 其实就是获取国际化配置文件i18n里面的属性值

 因为在控制器中需要获取里面的属性值,所注意就不能直接跳转,需要通过控制器,需要将上述mvc:view-controller标签删除,删除如下的内容:

    <mvc:view-controller path="/i18n" view-name="i18n"></mvc:view-controller>
    <mvc:view-controller path="/i18n2" view-name="i18n2"></mvc:view-controller>

同时,需要在Spring配置文件中添加SessionLocaleResolver的Bean,注意Bean的id,只能为localeResolver

    <!--
    配置SessionLocaleResolver,注意这里的SessionLocaleResolver的bean的id一定要为localeResolver,
    否则会报错:Cannot change HTTP accept header - use a different locale resolution strategy
    -->
    <bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"></bean>

在Controller中添加映射的方法:

@Autowired
private ResourceBundleMessageSource messageSource;

@RequestMapping("/i18n")
public String i18n(Locale locale) {
    //获取国际化配置文件中的属性值
    String val = messageSource.getMessage("i18n.user", null, locale);
    System.out.println(val);
    return "i18n";
}

配置完上述工作后,更改Internet选项中的中英文,后台可以打印出对应国际化语言中i18n.user属性值,当然也可以修改。

问题三 改变国际化的语言方式

 上述查看国际化效果必须通过设置Internet更改语言的方式,有点麻烦,当然也可以通过配置超连接直接从中文转成英文,需要配置LocaleChangeInterceptor拦截器,在SpringMVC的配置文件中,添加如下拦截器:

<!--配置LocaleChangeInterceptor拦截器,注意拦截器的配置方式-->
<mvc:interceptors>
    <bean  class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"></bean>
</mvc:interceptors>

这个拦截器会自动拦截i18n的配置文件,直接可以通过超链接来实现国际话效果的转化,跳转连接的写法:

<!--在首页上添加如下的超链接,注意必须通过locale属性的值指定国际化的标准是中文还是英文-->
  <a href="i18n?locale=zh_CH">中文</a><br>
  <a href="i18n?locale=en_US">英文</a><br>

配置完上述文件,即可直接访问页面通过超链接实现国际化效果的转换。

15. 关于文件上传

 在之前的小栗子中,已经做过一个文件上传和下载的小Demo,但是上面的HttpMessageConverter无法做到真正的上传(无法解析文件域和普通鱼),只能做下载,这里正式介绍在SpringMVC中,文件是如何上传的。SpringMVC为文件上传提供了直接支持,这种支持是通过即插即用的MultipartResolver接口实现的,Spring用Jakarta Commons FileUpload技术实现了一个MultipartResolver实现类:CommonsMultipartResolver,在Spring中默认没有装配MultipartResolver,如果想在SpringMVC中使用文件上传,需要做如下的配置:
 第一步,加入额外的jar包commons-fileupload-1.3.2.jar,这个jar依赖于CommonsIO,所以也要加入commons-io-2.5.jar;第二步,在SpringMVC中配置MultipartResolver:

    <!--配置MultipartResolver-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <!--配置一些其他属性,可以选择性配置-->
    <property name="defaultEncoding" value="utf-8"/>
    <!--注意这里最大上传限制的单位是KB,下面是配置的1M-->
    <property name="maxUploadSize" value="102400"/>
</bean>

然后在Controller中直接写上传的方法即可,注意使用MultipartFile直接获取文件的属性:

@RequestMapping("fileUpload")
public String testUpload(@RequestParam("file")MultipartFile file) throws IOException {
    System.out.println(file.getOriginalFilename());
    //有了文件的输入流就可以进行正常的上传了
    System.out.println(file.getInputStream());

    InputStream is = file.getInputStream();
    BufferedReader br = new BufferedReader(new InputStreamReader(is));

    File f = new File("C:\\Users\\weiguo Liu\\Desktop\\" + file.getOriginalFilename());
    OutputStream os = new FileOutputStream(f);

    String in = br.readLine();
    while(in!=null) {
        //注意这里读完一行往里面写的时候追加换行符!
        os.write((in + "\r\n") .getBytes());
        in = br.readLine();
    }

    if(br==null) {
        is.close();
        br.close();
    }

    if (os != null) {
        os.close();
    }

    return "success";
}

最后写一个页面简单的上传页面表单即可:

<form action="fileUpload" method="POST" enctype="multipart/form-data">
    File:<input type="file" name="file"/><br>
    <input type="submit" value="Submit"/><br>
</form>

好了文件上传就搞定了。

16. SpringMVC中的拦截器

 用户可以通过实现HandlerInterceptor接口自定义拦截器来对请求进行拦截处理,主要过程如下:
 第一步,创建拦截器,注意里面三个方法的使用

public class MyInterceptor implements HandlerInterceptor {
    /*在业务请求处理器之前被调用,如果在该拦截器对请求的URL处理后还要调用
    其他拦截器,或者是业务处理器去处理请求,那么返回true,如何在该拦截器调
    用完后不再调用其他组件处理该请求,那么返回false,此时目标方法也不会被执行!!

    若返回false:后续的拦截器和目标方法(控制器中映射URL的方法)不会被执行
    目标方法不被执行,所以前端页面也不会进行跳转,postHandle和afterCompletion
    两个方法也不会再执行了

    若返回true:后续的拦截器和目标方法会继续执行

    使用场景:做权限验证,如果没有权限就不调用目标方法;日志、事务等
     */
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        System.out.println("[MyInterceptor]==preHandle");
        return true;
    }

    /*
    在业务处理完请求后,但是在DispatcherServlet向客户端返回响应前被调用

    调用目标方法之后,但在渲染视图之前执行

    使用场景:可以对请求域中的属性或视图做修改
     */
    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
        System.out.println("[MyInterceptor]==postHandle");
    }

    /*在DispatcherServlet完全处理请求后被调用
    在渲染视图之后执行

    使用场景:释放资源
     */
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
        System.out.println("[MyInterceptor]==afterCompletion");
    }
}

 第二步,在SpringMVC配置文件中加入拦截器的配置:

<!--配置自定义拦截器-->
<mvc:interceptors>
    <bean class="com.hhu.interceptors.MyInterceptor"/>
</mvc:interceptors>

经过上述的配置后,拦截器即可工作,默认是拦截所有请求,如果需要拦截指定的URL请求或者不拦截指定的URL请求,可以将上述的配置改为:

<!--配置自定义拦截器-->
<mvc:interceptors>

    <!--拦截指定请求,不拦截指定URL也是一样-->
    <mvc:interceptor>
        <mvc:mapping path="/emps"/>
        <bean class="com.hhu.interceptors.MyInterceptor"/>
    </mvc:interceptor>

</mvc:interceptors>

经过上述的配置后,拦截器即可正常开启并工作了。
 上面是讨论了拦截器的正常配置方式,那如果有多个拦截器呢,在WEB基础那一块过滤器是一样的(执行顺序),记住S形即可,各个拦截器的各个方法执行流程如下所示:
这里写图片描述
 理清楚多个拦截器方法的执行顺序后,还需要考虑各个拦截中preHandle方法的返回值对于后续操作的影响,下面的讨论是基于两个拦截器都拦截的同一个URL而言,先看一下使用的配置文件

<!--配置自定义拦截器-->
<mvc:interceptors>
    <!--这个拦截器没有配置具体拦截请求,默认拦截所有请求-->
    <bean class="com.hhu.interceptors.MyInterceptor"/>

    <!--配置拦截器的拦截方式,这个拦截器指拦截/emps的请求-->
    <mvc:interceptor>
        <mvc:mapping path="/emps"/>
        <bean class="com.hhu.interceptors.MyInterceptor2"/>
    </mvc:interceptor>

</mvc:interceptors>

运行主要给出一下两个结论(其他两个省略,应该可以想到结果吧(ง •_•)ง):

  • Interceptor1的preHandle返回false,那么不管Interceptor2的preHandle返回true还是false,执行完Interceptor1的preHandle方法后,后面所有的拦截器和目标方法全部失效;
  • Interceptor1preHandle返回true,Interceptor2的preHandle返回false时,preHandler方法都会按上述的顺序执行,但是执行完该方法后,Interceptor2的工作就结束了,它的目标方法和它后面的拦截器(该案例中他后面没有拦截器)就不会执行了,而Interceptor1会直接执行自己的afterCompletion方法(注意:因为Interceptor2中preHandle返回false`,所以它们两个指向的同一个目标方法根本就没有执行,所以对于Interceptor1而言就会直接跳过自己的postHandle的方法,直接执行自己的afterCompletion方法最后结束)。

17.SpringMVC中异常的处理

 SpringMVC通过HandlerExceptionResolver处理程序的异常,包括Handler映射、数据绑定以及目标方法执行时发生的异常。HandlerExceptionResolver接口有如下的实现类:AbstractHandlerExceptionResolverAbstractHandlerMethodExceptionResolverAnnotationMethodHandlerExceptionResolverDefaultHandlerExceptionResolverExceptionHandlerExceptionResolverHandlerExceptionResolverCompositeResponseStatusExceptionResolverSimpleMappingExceptionResolver,其中AnnotationMethodHandlerExceptionResolver实现类已经过期,DispatcherServlet默认装配的DefaultHandlerExceptionResolver,如果SpringMVC的配置文件中没有配置

<mvc:annotation-driven/>

那么配置下面三个:

  • AnnotationMethodHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • DefaultHandlerExceptionResolver

如果配置了annotation-driven,那么配置下面三个:

  • ExceptionHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • DefaultHandlerExceptionResolver
    由于上面无配置的情况第一个Resolver已经丢弃了,所以正常的SpringMVC的配置文件都是加上如下配置的:
<mvc:annotation-driven/>

下面主要记录加了annotation-driven配置的三种ExceptionResolver。

17.1 @ExceptionHandler注解

 直接看一个小栗子:

/*
@ExceptionHandler注解中的异常用数组封装
注意如果想在页面展示错误信息,
不能用Map,只能用ModelAndView
    */
@ExceptionHandler({ArithmeticException.class})
public ModelAndView handleException(Exception e) {
    System.out.println("发生异常:" + e.getMessage());
    //将异常显示到error页面上,所以将其放到模型数据中,页面可以通过request中获取
    ModelAndView mv = new ModelAndView("error");
    mv.addObject("error", e);
    return mv;
}

@RequestMapping("/testException")
public String testException(@RequestParam("i") int i) {
    System.out.println("result:" + (10 / i));
    return "success";
}

页面为:

<a href="testException?i=10">testException</a><br>

在点击连接后将URL中的10改为0就可以发生除零异常,现然Controller的testException方法没有做任何处理,所以执行后会出现500页面,像上述代码中通过@ExceptionHandler注解对这个异常做了处理,并且在将异常信息放在请求域中,页面可以通过${error}的形式获取异常信息进行显示。

【注意】在封装异常信息的时候,只能通过ModelAndView的方式进行封装返回,不能通过Model或者Map(我也不知道为啥,先记着吧(ノへ ̄、))。
 上面已经将异常信息处理的方式显示出来了, 比较常用,在SpringMVC中,如果发生异常,它首先会先从当前的Handler(这里就是指的Controller)中寻找@ExceptionHandler注解来处理异常,如果找不到这个注解或找到注解但不能处理当前异常的,他就会去@ControllerAdvice注解的类中做同样的寻找工作,如果找不到就报错,换种方式说我们可以将上面Controller中的handleException方法移到其他的一个专门的类中进行处理也行,如下:

@ControllerAdvice
public class ExHandlers {

    @ExceptionHandler({ArithmeticException.class})
    public ModelAndView handleException(Exception e) {
        System.out.println("发生异常--->" + e.getMessage());
        ModelAndView mv = new ModelAndView("error");
        mv.addObject("error", e);
        return mv;
    }
}

17.2 @ResponseStatus注解

 在异常和异常父类中找到@ResponseStatus注解,然后使用这个注解中的属性进行处理,总之这个注解一般是用来修饰异常类的,在发生异常时,如果@ExceptionHandler注解没有解析异常,而发生的异常类是由@ResponseStatus注解修饰的,所以会使用ResponseStatusExceptionResolver解析到,最后在页面直接响应给客户,不需要像上面那样额外写error的页面。最后@ResponseStatus注解可以用来修饰异常类,也可以用来修饰Controller中的方法,好,看例子:
第一,创建自定义异常MyException,并用@ResponseStatus注解修饰:

//这个注解中的value和reason会显示到异常界面上
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "犯贱自定义异常")
public class MyException extends RuntimeException{
}

第二,控制器中抛出自定义的异常:

@RequestMapping("/responseError")
public String responseError(@RequestParam("i") int i) {
    if(i==13) {
        throw new MyException();
    }
    return "success";
}

访问Applicationcontext/responseError?i=13就可以看到定制的异常界面如下
这里写图片描述
 如果用这个注解修饰Conyroller中的方法的话,那么在访问这个映射路径的时候,不管该方法是否抛出异常,都会跳转到上述定制的错误页面而不会跳转到return的页面,但是方法中的逻辑还是正常执行,比如:

@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "犯贱自定义异常")
@RequestMapping("/i18n")
public String i18n(Locale locale) {
    String val = messageSource.getMessage("i18n.user", null, locale);
    System.out.println(val);
    return "i18n";
}

当访问Applicationcontext/i18n时,请求不会跳转到i18n指向的页面内容(URL是正常映射URL还是xx/i18n的形式,但是页面里面的内容变成上面的内容),而是直接显示上面的页面。

17.3 SimpleMappingExceptionResolver

 对于常见的异常,可以通过在SpringMVC的配置文件中配置SimpleMappingExceptionResolver,可以使其在发生对应错误的时候做出相应的处理,这里以数组下标越界为例做的的小Demo,配置文件如下:

<!--配置简单异常映射-->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">

    <property name="exceptionMappings">
        <props>
            <!--这里的异常信息必须写全类名,然后里面的value就是转向的页面error.jsp-->
            <prop key="java.lang.ArrayIndexOutOfBoundsException">error</prop>
        </props>
    </property>

    <!--配置异常的属性名,SimpleMappingExceptionResolver自动将发生的异常信息放到
    Request域中以供前端页面通过EL表达式获取信息,默认属性名为exception,如果对此不配置
    可以直接在前端页面通过${exception}的形式获取异常信息
    -->
    <property name="exceptionAttribute" value="error"/>
</bean>

在控制器中造一个异常出来:

@RequestMapping("/testSimpleEx")
public String testSimpleException(@RequestParam("i") int i) {
    String[] arr = new String[10] ;
    //这里可以模拟出数组越界的异常
    System.out.println(arr[i]);
    return "success";
}

前端页面

<!--如果在SpringMVC的配置文件中没有配置exceptionAttribute属性
那么这里的属性名就应该是exception
-->
${error}

访问Applicationcontext/testSimpleEx?i=13就会出现上面配置好的异常,而不需要我们手动将异常信息放到request域中,SimpleMappingExceptionResolver会自动往里面放,上面就是手动将异常信息放到ModelAndView中页面才能获取。

18. Spring整合SpringMVC

 作为Spring的子工程,SpringMVC和Spring整合时,一般Spring的配置文件放自己的东西以及其他框架的东西(数据源、事务、其他框架),而SpringMVC配置文件自己只用来配置SpringMVC,不两者夹杂,在web.xml文件中配置的时候可以用通配符来匹配Spring和SpringMVC的多个配置文件。
 首先如果只是单纯的配置文件的堆砌,很容易想到在Spring的配置文件applicationContext.xml和SpringMVC的配置文件springMVC.xml中都有context:component-scan这个扫描组件,在测试的时候发现,会有重复的基类包被扫描两次,这将导致基类包中被Spring管理的组件会被初始化两次(可以在组件中,写无参构造器观看)。为了解决这个问题,两个思路:
* 极致的使用MVC思想,将每个Dao层、Service层、Controller层等严格划分,这样就好了,DAO层和Service层交给Spring的配置文件去扫描,而Controller层交给SpringMVC扫描,这样就好了。
* 在Spring和SpringMVC两者配置文件扫描组件context:component-scan,使用他下面的子标签context:include-filtercontext:exclude-filter标签来配置它们各自需要扫描的包,使用type = annotation根据注解类型来扫描各自负责的类,如下:

<!--
    SpringMVC需要扫描的类包含两种:
    @Controller类和Controller异常处理类@ControllerAdvice
    注意关闭默认的过滤器
-->
<context:component-scan base-package="com.hhu" use-default-filters="false">
    <!--只扫描基类包下带如下注解的类-->
    <context:include-filter type="annotation" 
    expression="org.springframework.stereotype.Controller"/>

    <context:include-filter type="annotation" 
    expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
</context:component-scan>

<!--Spring的配置扫描就除去上述SpringMVC需要扫描的两类注解外全包-->
<context:component-scan base-package="com.hhu">
    <context:exclude-filter type="annotation"
     expression="org.springframework.stereotype.Controller"/>

    <context:exclude-filter type="annotation" 
    expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
</context:component-scan>


 在上面两种情况中,正常会觉得第一种想法很好,因为我们正常在写项目的时候就是这么搞的呀,分层,逻辑关系理的很清楚,又能方便这里Spring和Spring做重复扫描,但是在实际开发中,可能不是简单的划分为上述的Dao、Service、Controller等三层模型了,也可能是以功能模块为一层的,各个模块的的Dao、Service、Controller都放在自己基类层下,那这个时候应该按注解类型用第二种方法去分配扫描的任务。
【注意】SpringMVC中可以引用Spring管理的Bean实例,反过来Spring中的Bean是不能引用SpringMVC中Bean。

19. 关于SpringMVC和Struts2

 这两个经常拿来作比较,最直观的感受应该是SpringMVC在后台开发非常方便,而Struts2在页面开发比较方便;SpringMVC的入口是Servlet,而Struts2是Filter;SpringMVC比Struts2稍微快一点,因为SpringMVC是基于方法设计的,Struts2是基于类设计的,每次请求都会创建Action的新的实例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值