Spring MVC概念和使用

参考链接:

《Spring MVC 学习指南(第二版)》

Spring MVC 是 Spring 框架中用于Web应用快速开发的一个模块。Spring框架是一个开源框架,源码下载地址:

1
git clone git@github.com:spring-projects/spring-framework.git

控制反转(IOC)

DI和IOC是差不多的概念,一个重要特征是接口依赖,是把对象关系推迟到运行时去确定。Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。

IoC 不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

谁控制谁,控制什么:传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对 象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。

为何是反转,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。

依赖注入(DI)

IoC和DI由什么关系呢?其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC 而言,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”

谁依赖于谁:当然是应用程序依赖于IoC容器。

为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源。

谁注入谁:IoC容器注入应用程序某个对象,应用程序依赖的对象。

注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

假设类A依赖类B,A要使用B的实例必须先创建或者获取B的实例,如下:

1
2
3
4
5
6
7
8
class A{

public void importantMothod(){
B b = //... get an instance of B
b.usefulMethod();
//...
}
}

假设此时B不是一个具体的实体类而是一个抽象类或者接口,此时问题就复杂了,不能简单的在A内初始化B的实例。

依赖注入框架的引入就是来解决这个问题的,负责创建实例并将实例注入到A中,改写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A{

private B b;

public void importantMothod(){
b.usefulMethod();
//...
}

public void setB(B b){
this.b = b;
}
}

类A中多了一个setB方法,通过依赖注入框架来注入B的实例进来(构造函数注入也可以)。

在Spring中会先创建B,然后再创建A的实例,最后将B注入到A中。使用Spring程序会将几乎所有对象创建的工作交给Spring,并配置如何注入依赖。Spring支持xml配置和注解两种方式来配置。

Spring依赖注入的方式有两种:set方法(推荐)、构造器。下面以set注入为例:

1
2
3
4
<bean id="a" class="com.test.irain.A">
<property name="hello" ref="b"/>
</bean>
<bean id="b" class="com.test.irain.B"></bean>
1
2
3
4
5
6
7
8
public class A {

private B hello;

public void setHello(B hello) {
this.hello = hello;
}
}

面向切面(AOP)

下面摘抄于知乎上的一段关于面向切面的解释:https://www.zhihu.com/question/24863332

面向切面编程(AOP是Aspect Oriented Program的首字母缩写) ,我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。

但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。

也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。从技术上来说,AOP基本上是通过代理机制实现的。AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充。

面向切面AOP

接下来我们来看看AOP在Spring中的应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public interface UserService {

public void update();
public void delete();
public void save();
}

public class UserServiceImpl implements UserService {

public void update() {
System.out.println("更新用户信息");
}

public void delete() {
System.out.println("删除用户信息");
}

public void save() {
System.out.println("保存用户信息");
}
}

public class Test {

public static void main(String[] args){
ApplicationContext ac = new ClassPathXmlApplicationContext("aop.xml");
UserService userService = (UserService) ac.getBean("userservice");
userService.update();
}
}

新建aop.xml配置bean

1
<bean id="userservice" class="com.test.irain.UserServiceImpl"></bean>

假设,我们现在要添加一个操作日志记录功能,这个时候就要用到面向切面来实现了。

新建日志类

1
2
3
4
5
6
public class OptLogger {

public void logger(){
System.out.println("记录操作日志了...");
}
}

配置切面和切点

1
2
3
4
5
6
7
8
9
<bean id="userservice" class="com.test.irain.UserServiceImpl"></bean>
<bean id="optlogger" class="com.test.irain.OptLogger"></bean>

<aop:config>
<aop:pointcut id="servicepointcut" expression="execution(* com.test.irain.*(..))"/>
<aop:aspect id="loggeraspect" ref="optlogger"> <!-- 配置切面 -->
<aop:before method="logger" pointcut-ref="servicepointcut"/>
</aop:aspect>
</aop:config>

实现原理

Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用JDK Proxy去进行代理了(为啥?你写一个JDK Proxy的demo就知道了),这时候Spring AOP会使用Cglib,生成一个被代理对象的子类,来作为代理,放一张图出来就明白了:

SpringAOP过程

Spring MVC控制流程

SpringMVC流程图

从上面的图中我们可以大致明白springMVC的调度流程,下面详细说明:

  1. 发起请求到前端控制器(DispatcherServlet)。
  2. 前端控制器请求HandlerMapping查找Handler,可以根据xml配置、注解进行查找。

在web.xml中配置

1
2
3
4
5
6
7
8
9
10
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

  1. 处理器映射器HandlerMapping向前端控制器返回Handler。HandlerMapping会把请求映射为HandlerExecutionChain对象(包含一个Handler处理器(页面控制器)对象,多个HandlerInterceptor拦截器对象),通过这种策略模式,很容易添加新的映射策略。

还记得我们前一篇 《JavaWeb开发快速上手篇》 的如下配置吗,这个SimpleUrlHandlerMapping就是一个HandlerMapping的子类。

1
2
3
4
5
6
7
8
9
<bean id="simpleUrlHandlerMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<!-- /hello 路径的请求交给 id 为 helloController 的控制器处理-->
<prop key="/hello">helloController</prop>
</props>
</property>
</bean>
  1. 前端控制器调用处理器适配器(HandlerAdapter)去执行Handler。

Handler要实现Contoller接口,才能由处理器适配器执行

1
2
3
4
5
6
7
8
9
10
public class HelloController implements Controller {

@RequestMapping("/hello")
public ModelAndView handleRequest(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse) throws Exception {
ModelAndView mav = new ModelAndView("index.jsp");
mav.addObject("message", "Hello Spring MVC");
return mav;
}
}

配置Handler

1
<bean id="helloController" class="com.test.irain.controller.HelloController"></bean>
  1. 处理器适配器HandlerAdapter将会根据适配的结果去执行Handler。
  2. Handler执行完成后给适配器返回ModelAndView。
  3. 处理器适配器向前端控制器返回ModelAndView (ModelAndView是springmvc框架的一个底层对象,包括 Model和view)。
  4. 前端控制器请求视图解析器去进行视图解析 (根据逻辑视图名解析成真正的视图(jsp)),通过这种策略很容易更换其他视图技术,只需要更改视图解析器即可。
1
2
<!-- 视图解析器,解析jsp视图,默认使用jstl,classpath下需要有jstl的包 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"></bean>
  1. 视图解析器向前端控制器返回真正的视图View。
  2. 前端控制器请求进行视图渲染,视图渲染将模型数据(在ModelAndView)填充到request域。
  3. 前端控制器向用户响应结果。

Spring MVC 创建过程

Spring MVC的创建步骤如下:

第一步:加入相关jar包,必须用的包有:

1
2
3
4
5
6
7
spring-aop-xxx.RELEASE.jar
spring-beans-xxx.RELEASE.jar
spring-context-xxx.RELEASE.jar
spring-core-xxx.RELEASE.jar
spring-expression-xxx.RELEASE.jar
spring-web-xxx.RELEASE.jar
spring-webmvc-xxx.RELEASE.jar

另外可能还需要日志相关的jar包

1
common-logging-xxx.jar

第二步:在web.xml中配置DispatcherServlet.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 配置DispatcherServlet -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 可选:配置DispatcherServlet 的一个初始化参数,目的是配置Spring MVC配置文件的位置和名称 -->
<!-- 默认的配置文件在/WEB-INFO/<servlet-name>-servlet.xml -->
<init-param>
<param-name>contextConfigLocation</param-name>
<!-- 在src的根目录创建springmvc.xml配置文件 -->
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<!-- 设置加载的时候就创建spring mvc -->
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

第三步:加入Spring MVC 的配置文件(上面配置了该配置文件的路径和名称)。

第四步:编写处理请求的处理器,并标识为处理器。

在springmvc.xml(Spring MVC的配置文件)中配置自动扫描的包和视图解析器:

1
2
3
4
5
6
7
8
9
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="com.guohe.springmvc"></context:component-scan>

<!-- 配置视图解析器 -->
<!-- 对InternalResourceViewResolver解析器:解析为 profix + returnValue + suffix -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="profix" value="/WEB-INFO/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>

写一个控制器(使用@Controller标识):

1
2
3
4
5
6
7
8
9
10
@Controller
public class HelloWorld{
//使用@RequestMapping注解来映射请求的URL
@RequestMapping("/hello")
public String hello(){
System.out.println("hello world");
//这里对应的是/WEB-INFO/views/success.jsp
return "success";
}
}

第五步:编写视图(由上面配置可知,需要创建/WEB-INFO/views/success.jsp视图文件)。

RequestMapping用法

第一:@RequestMapping除了注解方法还可以注解类,这样请求路径就是 类的注解路径 + 方法注解路径

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping("springmvc")
@Controller
public class SpringMVCTest{

@RequestMapping("/hello")
public String hello(){
System.out.println("hello world");
return "success";
}
}
//请求路径是 localhost:8080/projname/springmvc/hello

第二:@RequestMapping除了可以使用请求URL映射请求外,还可以使用请求方式、请求参数、请求头映射请求。

映射对象映射说明
value请求URL
method请求方式
params请求参数
heads请求头
1
2
3
4
5
6
//指定请求方式为POST
@RequestMapping(value="/testmethod", method=RequestMethod.POST)
public String testMethod(){
//...
return "success";
}
1
2
3
4
5
6
7
8
9

//指定请求必须包含username参数,而且参数age的值不等于10
//例如请求连接: springmvc/testparam?username=xiaoming&age=8
@RequestMapping(value="/testparam", params={"username", "age!=10"},
heads={"Accept-Language=zh-CN,zh;q=0.8"})
public String testParamsAndHeads(){
//...
return "success";
}

第三:@PathVariable注解可以将URL中占位符参数绑定到控制器处理方法的入参中,这使得Spring MVC支持REST风格的URL.

1
2
3
4
5
6

@RequestMapping("/testPathVariable/{id}")
public String testPathVariable(@PathVariable("id") Integer id){
System.out.println("id = " + id)
return "success";
}

第四:@RequestParam可以绑定请求参数,类似的还有@RequestHead

1
2
3
4
5
6
7
@RequestMapping(value="/testRequestParam")
public String testRequestParam(@RequestParam(value="username") String un,
@RequestParam(value="age", required=false, defaultValue="0") int age){
System.out.println("username = " + username + ", age=" + age);
return "success";
}
//上面username参数是必须的,age参数不是必须的(设置了required=false)

第五:@CookieValue注解可以映射得到cookie的值。

1
2
3
4
5
6

@RequestMapping("/testCookieValue")
public String testCookieValue(@CookieValue("JSESSIONID") String sessionId){
System.out.println("sessionId = " + sessionId);
return "success";
}

输出模型

Spring MVC提供了以下几种途径输出模型数据:

  • ModelAndView:处理方法返回值类型为ModelAndView时,返回视图并且可添加模型数据。
  • Map及Model:实际上返回的是ExtendedModelMap对象。
  • @SessionAttributes:只能注解类,可以将属性放入session.
  • @ModelAttribute:标注可被应用在方法或方法参数上.
1
2
3
4
5
6
7

@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView(){
ModelAndView modelAndView = new ModelAndView("success");
modelAndView.addObject("name", "dp2px");
return modelAndView;
}
1
2
3
4
5
6

@RequestMapping("/testMap")
public String testMap(Map<String, Object> map){
map.put("name", "dp2px")
return "success";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//可以通过制定属性名,还可以通过模型的对象类型制定那些对象放入session
@SessionAttributes(value={"user"}, types={String.class})
@RequestMapping("/springmvc")
@Controller
public class SpringMVCTest{

@RequestMapping("/testSessionAttributes")
public String testSessionAttributes(Map<String, Object> map){
User user = new User("dp2px", "http://dp2px.com");
map.put("user", user);
map.put("stringtype", "i am string")
return "success";
}
}

在控制器的处理器方法参数上添加 @ModelAttribute 注释可以访问模型中的属性,如果不存在这个模型,则会自动将其实例化,产生一个新的模型。 模型属性还覆盖了来自 HTTP Servlet 请求参数的名称与字段名称匹配的值,也就是请求参数如果和模型类中的域变量一致,则会自动将这些请求参数绑定到这个模型对象,这被称为数据绑定,从而避免了解析和转换每个请求参数和表单字段这样的代码。

1
2
3
4
@RequestMapping("/modelAttribute")
public void getUser(@ModelAttribute("user") User user){
return "success";
}