# 前言

上篇文章介绍了 maven WEB 项目的搭建,基本的配置文件也都贴出来了,今天就来介绍下 SpringMVC 的工作原理以及工作中常用的注解。为以后开发打下坚实的基础。

# 正文

# SpringMVC 框架介绍

SpringMVC 就是通过 DispatcherServlet 将一堆组件串联起来的 Web 框架。

  • Spring MVC 属于 SpringFrameWork 的后续产品,已经融合在 Spring Web Flow 里面。

Spring 框架提供了构建 Web 应用程序的全功能 MVC 模块。使用 Spring 可插入的 MVC 架构,可以选择是使用内置的 Spring Web 框架还是 Struts 这样的 Web 框架。通过策略接口,Spring 框架是高度可配置的,而且包含多种视图技术,例如 JavaServer Pages(JSP)技术、Velocity、Tiles、iText 和 POI。Spring MVC 框架并不知道使用的视图,所以不会强迫您只使用 JSP 技术。
Spring MVC 分离了控制器、模型对象、分派器以及处理程序对象的角色,这种分离让它们更容易进行定制

  • Spring 的 MVC 框架主要由 DispatcherServlet、处理器映射、处理器 (控制器)、视图解析器、视图组成。

# SpringMVC 原理图

# SpringMVC 接口解释

DispatcherServlet 接口:
Spring 提供的前端控制器,所有的请求都有经过它来统一分发。在 DispatcherServlet 将请求分发给 Spring Controller 之前,需要借助于 Spring 提供的 HandlerMapping 定位到具体的 Controller。

HandlerMapping 接口:
能够完成客户请求到 Controller 映射。

Controller 接口:
需要为并发用户处理上述请求,因此实现 Controller 接口时,必须保证线程安全并且可重用。

Controller 将处理用户请求,这和 Struts Action 扮演的角色是一致的。一旦 Controller 处理完用户请求,则返回 ModelAndView 对象给 DispatcherServlet 前端控制器,ModelAndView 中包含了模型(Model)和视图(View)。

从宏观角度考虑,DispatcherServlet 是整个 Web 应用的控制器;从微观考虑,Controller 是单个 Http 请求处理过程中的控制器,而 ModelAndView 是 Http 请求过程中返回的模型(Model)和视图(View)。

ViewResolver 接口:
Spring 提供的视图解析器(ViewResolver)在 Web 应用中查找 View 对象,从而将相应结果渲染给客户。

# SpringMVC 运行原理

  1. 客户端请求提交到 DispatcherServlet

  2. 由 DispatcherServlet 控制器查询一个或多个 HandlerMapping,找到处理请求的 Controller

  3. DispatcherServlet 将请求提交到 Controller

  4. Controller 调用业务逻辑处理后,返回 ModelAndView

  5. DispatcherServlet 查询一个或多个 ViewResoler 视图解析器,找到 ModelAndView 指定的视图

  6. 视图负责将结果显示到客户端

DispatcherServlet 是整个 Spring MVC 的核心。它负责接收 HTTP 请求组织协调 Spring MVC 的各个组成部分。其主要工作有以下三项:

  1. 截获符合特定格式的 URL 请求。
  2. 初始化 DispatcherServlet 上下文对应的 WebApplicationContext,并将其与业务层、持久化层的 WebApplicationContext 建立关联。
  3. 初始化 Spring MVC 的各个组成组件,并装配到 DispatcherServlet 中。

结合项目理解:
1. 大家由上面原理也看明白了,DispatcherServlet 是整个 Spring MVC 的核心,SpringMVC 所有的请求都会通过这个前端控制器。它配置的地方是在 web.xml 里面,配置如下:

<servlet>
        <servlet-name>springmvctouchbaidu</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/applicationContext.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

配置的时候还指明了 contextConfigLocation,这样就会去加载这个 applicationContext.xml 了。

2. 原理第 2 点中由 DispatcherServlet 控制器查询一个或多个 HandlerMapping,找到处理请求的 Controller。这里其实是通过在 applicationContext-mvc.xml 配置了扫描路径以及开启了注解驱动来实现的。
applicationContext-mvc.xml 中的配置:

<context:component-scan base-package="com.tengj.demo"/>

context:component-scan 说明了要扫描 com.tengj.demo 这个包下所有的类。这里要注意一下,大家以后开发中有用到注解的类一定都要在这个 demo 包下,不然就会抛异常的。

加载了扫描路径后,还要开启注解驱动,这样才能认到代码中使用到的注解,比如 @Controller 这个注解。

<mvc:annotation-driven />

3.ViewResoler 视图解析器对应配置里面的

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/pages/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

这样,当 controller 中方法返回的是

return "index";

的时候,实际是指向了 / WEB-INF/pages/index.jsp 这个页面。

# 常用到的注解

当我们使用了自动扫描 + 注解的方式后,就不需要在 applicationContext-mvc.xml 里面配置类的 bean 了,要引用类直接在成员变量上面加行注解,set/get 方法也省了。超级方便,下面就列出常规开发中常用的注解。

# @Component

@Component
是所有受 Spring 管理组件的通用形式,@Component 注解可以放在类的头上,@Component 不推荐使用。

# 使用 @Controller 定义一个 Controller 控制器

@Controller 对应表现层的 Bean,也就是 Action,例如:

@Controller
public class UserController {
 ……
}

使用 @Controller 注解标识 UserController 之后,就表示要把 UserController 交给 Spring 容器管理,在 Spring 容器中会存在一个名字为”userController” 的 action,这个名字是根据 UserController 类名来取的。注意:如果 @Controller 不指定其 value【@Controller】,则默认的 bean 名字为这个类的类名首字母小写,如果指定 value【@Controller (value=”UserController”)】或者【@Controller (“UserController”)】,则使用 value 作为 bean 的名字。

# 使用 @Service 定义一个业务层 Bean

@Service 对应的是业务层 Bean,例如:

@Service("userService")
public class UserServiceImpl implements UserService{
………
}

@Service (“userService”) 注解是告诉 Spring,当 Spring 要创建 UserServiceImpl 的的实例时,bean 的名字必须叫做”userService”,这样当 Action 需要使用 UserServiceImpl 的的实例时,就可以由 Spring 创建好的”userService”,然后注入给 Action:在 Action 只需要声明一个名字叫 “userService” 的变量来接收由 Spring 注入的”userService” 即可,具体代码如下:

// 注入 userService
@Resource(name="userService")
UserService userService;

注意:在 UserController 声明的 “userService” 变量的类型必须是 “UserServiceImpl” 或者是其父类 “UserService”,否则由于类型不一致而无法注入,由于 UserController 中的声明的 “userService” 变量使用了 @Resource 注解去标注,并且指明了其 name = “userService”,这就等于告诉 Spring,说我 UserController 要实例化一个 “userService”,你 Spring 快点帮我实例化好,然后给我,当 Spring 看到 userService 变量上的 @Resource 的注解时,根据其指明的 name 属性可以知道,UserController 中需要用到一个 UserServiceImpl 的实例,此时 Spring 就会把自己创建好的名字叫做”userService” 的 UserServiceImpl 的实例注入给 UserController 中的 “userService” 变量,帮助 UserController 完成 userService 的实例化,这样在 UserController 中就不用通过 “UserService userService = new UserServiceImpl ();” 这种最原始的方式去实例化 userService 了。

如果没有 Spring,那么当 UserController 需要使用 UserServiceImpl 时,必须通过 “UserService userService = new UserServiceImpl ();” 主动去创建实例对象,但使用了 Spring 之后,UserController 要使用 UserServiceImpl 时,就不用主动去创建 UserServiceImpl 的实例了,创建 UserServiceImpl 实例已经交给 Spring 来做了,Spring 把创建好的 UserServiceImpl 实例给 UserController,UserController 拿到就可以直接用了。

UserController 由原来的主动创建 UserServiceImpl 实例后就可以马上使用,变成了被动等待由 Spring 创建好 UserServiceImpl 实例之后再注入给 UserController,UserController 才能够使用。这说明 UserController 对 “UserServiceImpl” 类的 “控制权” 已经被 “反转” 了,原来主动权在自己手上,自己要使用 “UserServiceImpl” 类的实例,自己主动去 new 一个出来马上就可以使用了,但现在自己不能主动去 new “UserServiceImpl” 类的实例,new “UserServiceImpl” 类的实例的权力已经被 Spring 拿走了,只有 Spring 才能够 new “UserServiceImpl” 类的实例,而 UserController 只能等 Spring 创建好 “UserServiceImpl” 类的实例后,再 “恳求” Spring 把创建好的 “UserServiceImpl” 类的实例给他,这样他才能够使用 “UserServiceImpl”,这就是 Spring 核心思想 “控制反转”,也叫 “依赖注入”。

“依赖注入” 也很好理解,UserController 需要使用 UserServiceImpl 干活,那么就是对 UserServiceImpl 产生了依赖,Spring 把 Acion 需要依赖的 UserServiceImpl 注入 (也就是 “给”) 给 UserController,这就是所谓的 “依赖注入”。对 UserController 而言,UserController 依赖什么东西,就请求 Spring 注入给他,对 Spring 而言,UserController 需要什么,Spring 就主动注入给他。

# 使用 @Repository 定义一个数据访问层 Bean

@Repository 对应数据访问层 Bean ,例如:

@Repository(value="userDao")
 public class UserDao {
 ………
 }

@Repository (value=”userDao”) 注解是告诉 Spring,让 Spring 创建一个名字叫 “userDao” 的 UserDao 实例。

当 Service 需要使用 Spring 创建的名字叫 “userDao” 的 UserDao 实例时,就可以使用 @Resource (name = “userDao”) 注解告诉 Spring,Spring 把创建好的 userDao 注入给 Service 即可。

// 注入 userDao
@Resource(name = "userDao")
private UserDao userDao;

# @Resource 跟 @Autowired 比较

上面介绍中 Controller 中注入 userService 或者 Service 层里面注入 dao 都是用 @Resource 标签,其实也可以使用 @Autowired 来替代,但是建议使用 @Resource。下面说说这 2 者的区别:

  1. @Autowired 和 @Resource 都可以用来装配 bean,都可以写在字段上,或者方法上。
  2. @Autowired 属于 Spring 的;@Resource 为 JSR-250 标准的注释,属于 J2EE 的。
  3. @Autowired 默认按类型装配,默认情况下必须要求依赖对象必须存在,如果要允许 null 值,可以设置它的 required 属性为 false,例如:@Autowired (required=false) ,如果我们想使用名称装配可以结合 @Qualifier 注解进行使用
    例如:
@Autowired
@Qualifier("baseDao")
private BaseDao baseDao;
  1. @Resource,默认按照名称进行装配,名称可以通过 name 属性进行指定,如果没有指定 name 属性,当注解写在字段上时,默认取字段名进行安装名称查找,如果注解写在 setter 方法上默认取属性名进行装配。当找不到与名称匹配的 bean 时才按照类型进行装配。但是需要注意的是,如果 name 属性一旦指定,就只会按照名称进行装配。
    例如:
@Resource(name="baseDao")
private BaseDao baseDao;
  1. 之所以推荐使用 @Resource,因为这个注解是属于 J2EE 的,减少了与 spring 的耦合。这样代码看起就比较优雅。

# 使用 @RequestMapping 来映射 Request 请求与处理器

SpringMVC 使用 @RequestMapping 注解为控制器制定可以处理哪些 URL 请求
在控制器的类定义及方法定义处都可以标注

  • 类定义处:提供初步的请求映射信息。相对于 WEB 应用的根目录
  • 方法处:提供进一步的细分映射信息。相对于类定义处的 URL,若类定义处未标注 @RequestMapping,则方法处标记的 URL 相对于 WEB 应用的根目录。

举个列子:

@Controller
@RequestMapping(value="/test")
public class UserController{
    @RequestMapping(value="/view",method = RequestMethod.GET)
    public String index(){
        System.out.println("进来了");
        return "index";
    }
}

上面这样,只要地址访问 http://localhost:8080/SpringMVCMybatis/test/view 就能进入这个 index 方法了,其中使用 method 属性来指定请求是 get 还是 post。

# (一)使用带占位符 URI@PathVariable

带占位符的 URL 是 Spring3.0 新增的功能,该功能在 SpringMVC 向 REST 目标挺进发展过程中具有里程碑的意义

通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中:URL 中的 {xxx} 占位符可以通过 @PathVariable (“xxx”) 绑定到操作方法入参中。
例子:

/**
 * @RequestMapping 可以来映射 URL 中的占位符到目标方法的参数中
 * @param id
 * @return
 */
@RequestMapping("/testPathVariable/{id}")
public String testPathVariable(@PathVariable("id") String id){
    System.out.println("testPathVariable id="+id);
    return "index";
}

# (二)使用 @RequestParam 绑定 HttpServletRequest 请求参数到控制器方法参数

@RequestMapping ( "requestParam" )
   public String testRequestParam( @RequestParam(required=false) String name, @RequestParam ( "age" ) int age) {
       return "requestParam" ;
}

在上面代码中利用 @RequestParam 从 HttpServletRequest 中绑定了参数 name 到控制器方法参数 name ,绑定了参数 age 到控制器方法参数 age 。值得注意的是和 @PathVariable 一样,当你没有明确指定从 request 中取哪个参数时,Spring 在代码是 debug 编译的情况下会默认取更方法参数同名的参数,如果不是 debug 编译的就会报错。此外,当需要从 request 中绑定的参数和方法的参数名不相同的时候,也需要在 @RequestParam 中明确指出是要绑定哪个参数。在上面的代码中如果我访问 /requestParam.do?name=hello&age=1 则 Spring 将会把 request 请求参数 name 的值 hello 赋给对应的处理方法参数 name ,把参数 age 的值 1 赋给对应的处理方法参数 age 。

在 @RequestParam 中除了指定绑定哪个参数的属性 value 之外,还有一个属性 required ,它表示所指定的参数是否必须在 request 属性中存在,默认是 true ,表示必须存在,当不存在时就会报错。在上面代码中我们指定了参数 name 的 required 的属性为 false ,而没有指定 age 的 required 属性,这时候如果我们访问 /requestParam.do 而没有传递参数的时候,系统就会抛出异常,因为 age 参数是必须存在的,而我们没有指定。而如果我们访问 /requestParam.do?age=1 的时候就可以正常访问,因为我们传递了必须的参数 age ,而参数 name 是非必须的,不传递也可以。

# (三)使用 @CookieValue 绑定 cookie 的值到 Controller 方法参数

@RequestMapping ( "cookieValue" )
    public String testCookieValue( @CookieValue ( "hello" ) String cookieValue, @CookieValue String hello) {
       System. out .println(cookieValue + "-----------" + hello);
       return "cookieValue" ;
    }

在上面的代码中我们使用 **@CookieValue** 绑定了 cookie 的值到方法参数上。上面一共绑定了两个参数,一个是明确指定要绑定的是名称为 hello 的 cookie 的值,一个是没有指定。使用没有指定的形式的规则和 **@PathVariable**、@RequestParam 的规则是一样的,即在 debug 编译模式下将自动获取跟方法参数名同名的 cookie 值。

# (四)使用 @RequestHeader 注解绑定 HttpServletRequest 头信息到 Controller 方法参数

@RequestMapping ( "testRequestHeader" )
public String testRequestHeader( @RequestHeader ( "Host" ) String hostAddr, @RequestHeader String Host, @RequestHeader String host ) {
    System. out .println(hostAddr + "-----" + Host + "-----" + host );
    return "requestHeader" ;
}

在上面的代码中我们使用了 @RequestHeader 绑定了 HttpServletRequest 请求头 host 到 Controller 的方法参数。上面方法的三个参数都将会赋予同一个值,由此我们可以知道在绑定请求头参数到方法参数的时候规则和 @PathVariable@RequestParam 以及 @CookieValue 是一样的,即没有指定绑定哪个参数到方法参数的时候,在 debug 编译模式下将使用方法参数名作为需要绑定的参数。但是有一点 @RequestHeader 跟另外三种绑定方式是不一样的,那就是在使用 @RequestHeader 的时候是大小写不敏感的,即 @RequestHeader (“Host”) 和 @RequestHeader (“host”) 绑定的都是 Host 头信息。记住在 @PathVariable@RequestParam@CookieValue 中都是大小写敏感的。

# (五)@RequestMapping 的一些高级应用

在 RequestMapping 中除了指定请求路径 value 属性外,还有其他的属性可以指定,如 params 、method 和 headers 。这样属性都可以用于缩小请求的映射范围。

# 1.params 属性
@RequestMapping (value= "testParams" , params={ "param1=value1" , "param2" , "!param3" })
    public String testParams() {
       System. out .println( "test Params..........." );
       return "testParams" ;
    }

在上面的代码中我们用 @RequestMapping 的 params 属性指定了三个参数,这些参数都是针对请求参数而言的,它们分别表示参数 param1 的值必须等于 value1 ,参数 param2 必须存在,值无所谓,参数 param3 必须不存在,只有当请求 /testParams.do 并且满足指定的三个参数条件的时候才能访问到该方法。所以当请求 /testParams.do?param1=value1&param2=value2 的时候能够正确访问到该 testParams 方法,当请求 /testParams.do?param1=value1&param2=value2&param3=value3 的时候就不能够正常的访问到该方法,因为在 @RequestMapping 的 params 参数里面指定了参数 param3 是不能存在的。

# 2.method 属性
@RequestMapping (value= "testMethod" , method={RequestMethod. GET , RequestMethod. DELETE })
    public String testMethod() {
       return "method" ;
    }

在上面的代码中就使用 method 参数限制了以 GET 或 DELETE 方法请求 /testMethod.do 的时候才能访问到该 Controller 的 testMethod 方法。

# 3.headers 属性
@RequestMapping (value= "testHeaders" , headers={ "host=localhost" , "Accept" })
    public String testHeaders() {
       return "headers" ;
    }

headers 属性的用法和功能与 params 属性相似。在上面的代码中当请求 /testHeaders.do 的时候只有当请求头包含 Accept 信息,且请求的 host 为 localhost 的时候才能正确的访问到 testHeaders 方法。

# (六)以 @RequestMapping 标记的处理器方法支持的方法参数和返回类型

# 1. 支持的方法参数类型
  1. HttpServlet 对象,主要包括 HttpServletRequest 、HttpServletResponse 和 HttpSession 对象。 这些参数 Spring 在调用处理器方法的时候会自动给它们赋值,所以当在处理器方法中需要使用到这些对象的时候,可以直接在方法上给定一个方法参数的申明,然后在方法体里面直接用就可以了。但是有一点需要注意的是在使用 HttpSession 对象的时候,如果此时 HttpSession 对象还没有建立起来的话就会有问题。

  2. Spring 自己的 WebRequest 对象。 使用该对象可以访问到存放在 HttpServletRequest 和 HttpSession 中的属性值。

  3. InputStream 、OutputStream 、Reader 和 Writer 。 InputStream 和 Reader 是针对 HttpServletRequest 而言的,可以从里面取数据;OutputStream 和 Writer 是针对 HttpServletResponse 而言的,可以往里面写数据。

  4. 使用 @PathVariable 、@RequestParam 、@CookieValue 和 @RequestHeader 标记的参数。

  5. 使用 @ModelAttribute 标记的参数。

  6. java.util.Map 、Spring 封装的 Model 和 ModelMap 。 这些都可以用来封装模型数据,用来给视图做展示。

  7. 实体类。 可以用来接收上传的参数。

  8. Spring 封装的 MultipartFile 。 用来接收上传文件的。

  9. Spring 封装的 Errors 和 BindingResult 对象。 这两个对象参数必须紧接在需要验证的实体对象参数之后,它里面包含了实体对象的验证结果。

# 2. 支持的返回类型
  1. 一个包含模型和视图的 ModelAndView 对象。

  2. 一个模型对象,这主要包括 Spring 封装好的 Model 和 ModelMap ,以及 java.util.Map ,当没有视图返回的时候视图名称将由 RequestToViewNameTranslator 来决定。

  3. 一个 View 对象。这个时候如果在渲染视图的过程中模型的话就可以给处理器方法定义一个模型参数,然后在方法体里面往模型中添加值。

  4. 一个 String 字符串。这往往代表的是一个视图名称。这个时候如果需要在渲染视图的过程中需要模型的话就可以给处理器方法一个模型参数,然后在方法体里面往模型中添加值就可以了。

  5. 返回值是 void 。这种情况一般是我们直接把返回结果写到 HttpServletResponse 中了,如果没有写的话,那么 Spring 将会利用 RequestToViewNameTranslator 来返回一个对应的视图名称。如果视图中需要模型的话,处理方法与返回字符串的情况相同。

  6. 如果处理器方法被注解 @ResponseBody 标记的话,那么处理器方法的任何返回类型都会通过 HttpMessageConverters 转换之后写到 HttpServletResponse 中,而不会像上面的那些情况一样当做视图或者模型来处理。

  7. 除以上几种情况之外的其他任何返回类型都会被当做模型中的一个属性来处理,而返回的视图还是由 RequestToViewNameTranslator 来决定,添加到模型中的属性名称可以在该方法上用 @ModelAttribute (“attributeName”) 来定义,否则将使用返回类型的类名称的首字母小写形式来表示。使用 @ModelAttribute 标记的方法会在 @RequestMapping 标记的方法执行之前执行。

# (七)使用 @ModelAttribute 和 @SessionAttributes 传递和保存数据

SpringMVC 支持使用 @ModelAttribute@SessionAttributes 在不同的模型和控制器之间共享数据。 @ModelAttribute 主要有两种使用方式,一种是标注在方法上,一种是标注在 Controller 方法参数上。

@ModelAttribute 标记在方法上的时候,该方法将在处理器方法执行之前执行,然后把返回的对象存放在 session 或模型属性中,属性名称可以使用 @ModelAttribute (“attributeName”) 在标记方法的时候指定,若未指定,则使用返回类型的类名称(首字母小写)作为属性名称。关于 @ModelAttribute 标记在方法上时对应的属性是存放在 session 中还是存放在模型中,我们来做一个实验,看下面一段代码。

@Controller
@RequestMapping ( "/myTest" )
public class MyController {
    @ModelAttribute ( "hello" )
    public String getModel() {
       System. out .println( "-------------Hello---------" );
       return "world" ;
    }
    @ModelAttribute ( "intValue" )
    public int getInteger() {
       System. out .println( "-------------intValue---------------" );
       return 10;
    }
    @RequestMapping ( "sayHello" )
    public void sayHello( @ModelAttribute ( "hello" ) String hello, @ModelAttribute ( "intValue" ) int num, @ModelAttribute ( "user2" ) User user, Writer writer, HttpSession session) throws IOException {
       writer.write( "Hello " + hello + " , Hello " + user.getUsername() + num);
       writer.write( "\r" );
       Enumeration enume = session.getAttributeNames();
       while (enume.hasMoreElements())
           writer.write(enume.nextElement() + "\r" );
    }
    @ModelAttribute ( "user2" )
    public User getUser() {
       System. out .println( "---------getUser-------------" );
       return new User(3, "user2" );
    }
}

当我们请求 /myTest/sayHello.do 的时候使用 @ModelAttribute 标记的方法会先执行,然后把它们返回的对象存放到模型中。最终访问到 sayHello 方法的时候,使用 @ModelAttribute 标记的方法参数都能被正确的注入值。执行结果如下所示:

Hello world,Hello user210

由执行结果我们可以看出来,此时 session 中没有包含任何属性,也就是说上面的那些对象都是存放在模型属性中,而不是存放在 session 属性中。那要如何才能存放在 session 属性中呢?这个时候我们先引入一个新的概念 @SessionAttributes ,它的用法会在讲完 @ModelAttribute 之后介绍,这里我们就先拿来用一下。我们在 MyController 类上加上 @SessionAttributes 属性标记哪些是需要存放到 session 中的。看下面的代码:

@Controller
@RequestMapping ( "/myTest" )
@SessionAttributes (value={ "intValue" , "stringValue" }, types={User. class })
public class MyController {
    @ModelAttribute ( "hello" )
    public String getModel() {
       System. out .println( "-------------Hello---------" );
       return "world" ;
    }
    @ModelAttribute ( "intValue" )
    public int getInteger() {
       System. out .println( "-------------intValue---------------" );
       return 10;
    }
   
    @RequestMapping ( "sayHello" )
    public void sayHello(Map<String, Object> map, @ModelAttribute ( "hello" ) String hello, @ModelAttribute ( "intValue" ) int num, @ModelAttribute ( "user2" ) User user, Writer writer, HttpServletRequest request) throws IOException {
       map.put( "stringValue" , "String" );
       writer.write( "Hello " + hello + " , Hello " + user.getUsername() + num);
       writer.write( "\r" );
       HttpSession session = request.getSession();
       Enumeration enume = session.getAttributeNames();
       while (enume.hasMoreElements())
           writer.write(enume.nextElement() + "\r" );
       System. out .println(session);
    }
    @ModelAttribute ( "user2" )
    public User getUser() {
       System. out .println( "---------getUser-------------" );
       return new User(3, "user2" );
    }
}

在上面代码中我们指定了属性为 intValue 或 stringValue 或者类型为 User 的都会放到 Session 中,利用上面的代码当我们访问 /myTest/sayHello.do 的时候,结果如下:

Hello world,Hello user210

仍然没有打印出任何 session 属性,这是怎么回事呢?怎么定义了把模型中属性名为 intValue 的对象和类型为 User 的对象存到 session 中,而实际上没有加进去呢?难道我们错啦?我们当然没有错,只是在第一次访问 /myTest/sayHello.do 的时候 @SessionAttributes 定义了需要存放到 session 中的属性,而且这个模型中也有对应的属性,但是这个时候还没有加到 session 中,所以 session 中不会有任何属性,等处理器方法执行完成后 Spring 才会把模型中对应的属性添加到 session 中。所以当请求第二次的时候就会出现如下结果:

Hello world,Hello user210
user2
intValue
stringValue

当 @ModelAttribute 标记在处理器方法参数上的时候,表示该参数的值将从模型或者 Session 中取对应名称的属性值,该名称可以通过 @ModelAttribute (“attributeName”) 来指定,若未指定,则使用参数类型的类名称(首字母小写)作为属性名称。

# 总结

到此,SpringMVC 的原理以及常用注解就介绍的差不多了,平时开发这些就够用了,如果你还想深入学习 SpringMVC 知识点,可以关注我个人公众号,里面资源贴有全套的视频教程。

参考
Spring 常用注解
@AUTOWIRED 与 @RESOURCE 的区别
SpringMVC Controller 介绍及常用注解