spring mvc 简洁易学。但自从 spring 4 开始,案例都采用 spring boot,无论老鸟菜鸟都懵了啊。本文介绍 mvc 的框架原理,使用简单案例分别使用spring boot 容器、其他 servlet 容器(jetty)的xml配置、无 xml 配置实现,讨论 spring4 mvc 不同环境实现程序之间的区别和联系。由于每种方式不是代码兼容的(蛋疼),这对 servlet 程序员来说是痛苦的负担,两套体系的挣扎。但对于新人,则可以选择自己喜欢的方式快速实现 web mvc 编程。
一、 MVC 的概念
1. 什么是 MVC
MVC是一种结构风格,就是将程序的输入、处理、输出这个顺序过程抽象为模型、视图、控制三个部分。程序员仅需关注业务的数据、展现、流程三个元素,将 Context,通讯协议、进程与线程、会话等复杂的要素交给框架处理,实现业务为中心的编程理念。在 web 应用中,MVC的含义是:
- M(模型/Model):业务涉及的数据对象实例。例如,你显示一定订单,它包含用户、订单、订单项、支付等业务对象数据;
- V(视图/View):展示业务人机交互界面的显示模板。例如,jsp 文件等,它能将模型中的数据填入显示模板,用户可看到界面元素丰富的界面;
- C(控制器/Controller):用户输入处理单元。它检查输入的合法性,处理输入表单,按流程调用业务函数,生成输出需要的数据模型,选择视图模板,输出。
使用MVC结构编程,使得程序业务逻辑清晰,模块结构好。对提升开发效率,增强可维护性,促进团队内部按技能分工,起到关键作用,因此几乎所有语言都有自己若干不同的 MVC 支持框架实现。
2. MVC 在 web 程序中的位置
对于新手,常会对某种语言或 MVC 框架产生崇拜,例如学了 MVC 就掌握了 web 编程,喜欢对比 struts 和 spring mvc 的效率。本节的目的是供新手了解 MVC 结构在 web 程序中的位置。附图表示了 MVC 结构在 web 开发中的位置。
无论是 J2EE 或 轻量级 web 应用,大概分为三个层次:
- 表示层(present layer): 处理 HTTP 输入(Request),然后调用业务服务,产生输出(Response)。这里,应用开发人员只需要考虑 MVC 三个编程元素。
- 业务层(business layer): 提供满足 ACID 要求的业务服务。这里,开发人员只需声明对外的业务服务函数,spring 等提供事务(Transaction)支持。
- 持久化层(persistence layer):提供数据表存取机制,主要是 ORM 框架实现以对象-关系数据库的映射。这里,开发人员只需声明表和对象的映射,特殊的 SQL。
因此,MVC结构仅是 web 开发中表示层技术。
JAVA 所有框架都尊崇这样的理念,“应用开发人员关注于业务逻辑的实现而不是底层的实现机制”。JAVA 开发一般是最佳实践驱动,即文件名、类名、目录结构、包结构都有自己一套长期形成的默认规则,这是初学者最大的学习障碍(demo 程序不一定遵守这些规则)。最佳的办法就是在看靠谱的项目代码,学习 Java 设计模式。例如:一个类中出现 process(...) 方法,你立马就知道这时命令模式(command pattern)的实现,必定实现了一个 xxxCommand 接口。
3. MVC 部件执行过程
各种语言 web MVC 框架不可胜数,但执行过程都是一致的。
- 分配/映射:这时框架完成的,它根据客户端 URL 映射到你定义的控制器类(使用 @ 或 外部 xml),做部分 Valid 检查
- 控制器:执行业务流程,一般步骤是:
- 参数检查
- 业务服务访问
- 装配数据模型
- 选择输出模板
- 模板渲染:模板按数据模型提供的数据渲染,生成网页或特定数据格式
因此,你需要一些 HTTP 基础知识;URL 到函数的映射;模板渲染表达式。
4. 选择 MVC 框架
尽管 spring MVC 的效率是一直被质疑,反射用的越多的框架效率也差一些,但使用会更方便。
选择 MVC 框架第一准则是团队培训成本,即你团队最优秀的人会什么,大家都用什么。第二准则是简单实用,简单实用是相对感受=没标准,个人喜欢 HTTP Request 与 处理函数的 Mapping 模型(如 spring MVC),这与 Restful Service 开发泛型是一致的,是现代 web 开发最流行的方式。
二、用 spring boot 实现 spring4 mvc
这里使用的 Spring 官方案例,参见:http://spring.io/guides/gs/serving-web-content/ 。可直接从 GIT 下载代码。
开发环境 JAVA JDK 1.8, Maven 3.3 +。 如果你使用 Centos 7,可参见:http://blog.csdn.net/pmlpml/article/details/53427164
1. 程序结构
对于刚学 Java 的来说,这与普通 Java 程序几乎没有区别。程序在 /src/main/java 源代码目录下;静态资源与视图模板都 /src/main/resources 目录下;maven 文件 pom.xml。
2. 控制器与模型
@Controller
public class GreetingController {
@RequestMapping("/greeting")
public String greeting(@RequestParam(value="name", required=false, defaultValue="World") String name, Model model) {
model.addAttribute("name", name);
return "greeting";
}
}
GreetingController 代码非常简单,要点:
- @Controller 注释申明这个类是控制器
- @RequestMapping("/greeting") 注释申明 URL 匹配 “/greeting” 给这个函数处理。
- @RequestParam(value="name", required=false, defaultValue="World") 注释申明如果查询字符串中有 name=value 则 value 转为 String 给 name 这个参数。例如:URL 是 /greeting?name=User 则变量 name = “User” 。
- model 是一个容器,存放变量供视图模板使用
- 返回 “greeting” 是模板名称。
3. 视图
greeting.html 在资源文件夹模板目录下:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Getting Started: Serving Web Content</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'Hello, ' + ${name} + '!'" />
</body>
</html>
这是 thymeleaf 风格的模板。其中 th:text=“表达式” 表示修改 <p> 元素的 text 属性。 ${name} 从模型中获取对象 name。
4. 程序启动代码
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
非常简单,要点:
- @SpringBootApplication 注释申明了这个类,等价于四个注释。
- main()方法中,SpringApplication.run() 这个配置的程
其实,这个程序有许多默认的配置,如:模板的目录,静态文件的目录。如果要修改这些,可能要读 spring boot 上百页文档。例如,要添加一个过滤器?请阅读:http://docs.spring.io/spring-boot/docs/1.4.2.RELEASE/reference/htmlsingle/#boot-features-embedded-container
这对熟悉 servlet 的程序猿,无疑是痛苦的负担,除非你打算成为 spring boot 的专责程序猿。
5. pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>spring-mvc-demo</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
它最大好处就是依赖配置简单,几乎 spring 所有模块都下载了。
6. 运行
一般情况下,使用 maven 运行,debug 除外。在 pom.xml 目录输入命令:
mvn spring-boot:run
程序运行,web 服务器启动在 8080 端口。输入:
curl http://localhost:8080/greeting?name=User
三、用传统 XML 配置实现 spring4 mvc
1. 程序结构
这是一个传统的 Spring MVC 程序,也是标准的 web servlet 2.3 + 的程序结构。java 源代码目录 /src/main/java;默认资源目录变为 /src/main/webapp/WEB-INF,其中 web.xml 是 servlet 应用配置,hello-servlet.xml 是 spring beans 的配置。
2. web servlet 应用启动配置
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- spring mvc 静态资源 404问题 : http://blog.csdn.net/this_super/article/details/7884383 -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
这对新手可能理解困难点。servlet 启动了 DispatcherServlet,它的名字 hello,处理的 url 是 “/” 开头的所有 URL,静态资源使用 default Servlet。
由于没有定义<listener>, DispatcherServlet 默认采用 name-servlet.xml 最为 spring 配置文件,这里是 hello-servlet.xml.
3. spring 配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<mvc:annotation-driven/>
<context:component-scan base-package="hello"/>
<!-- SpringResourceTemplateResolver automatically integrates with Spring's own -->
<!-- resource resolution infrastructure, which is highly recommended. -->
<bean id="templateResolver"
class="org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver">
<property name="prefix" value="/WEB-INF/templates/" />
<property name="suffix" value=".html" />
<!-- HTML is the default value, added here for the sake of clarity. -->
<property name="templateMode" value="HTML" />
<!-- Template cache is true by default. Set to false if you want -->
<!-- templates to be automatically updated when modified. -->
<property name="cacheable" value="true" />
</bean>
<!-- SpringTemplateEngine automatically applies SpringStandardDialect and -->
<!-- enables Spring's own MessageSource message resolution mechanisms. -->
<bean id="templateEngine"
class="org.thymeleaf.spring4.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver" />
<!-- Enabling the SpringEL compiler with Spring 4.2.4 or newer can speed up -->
<!-- execution in most scenarios, but might be incompatible with specific -->
<!-- cases when expressions in one template are reused across different data -->
<!-- ypes, so this flag is "false" by default for safer backwards -->
<!-- compatibility. -->
<property name="enableSpringELCompiler" value="true" />
</bean>
<bean id="viewResolver" class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
<property name="templateEngine" ref="templateEngine" />
</bean>
</beans>
这里配置略微复杂,要点:
- <context:annotation-config/> 对应 @configuration
- <mvc:annotation-driven/> 对应 @EnableWebMvc
- <context:component-scan base-package="hello"/> 对应 @ComponentScan
- 后面是 thymeleaf 与 spring4 集成需要的配置。 参见 thymeleaf 官网
这样,Spring Dispatcher 就能使用 @注释,分配/映射 URL 并使用 thymeleaf 引擎渲染页面。
4. 控制器
@Controller
public class GreetingController {
@RequestMapping("/greeting")
public String greeting(@RequestParam(value = "name", required = false, defaultValue = "World") String name,
Model model) {
model.addAttribute("name", name);
return "greeting";
}
@RequestMapping("/hello")
public void sayHello(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().println("<h1>Hello ,!!!!</h1>");
response.getWriter().println("session=" + request.getSession(true).getId());
}
// http://stackoverflow.com/questions/7415084/spring-welcome-file-list-correct-mapping
@RequestMapping("/")
public String root() {
return "redirect:/index.html";
}
}
控制器程序几乎没有变化。只是为了支持 welcome 页面,以及使用传统 servlet 方式给出了映射。
- /hello 可以让你自由处理输入输出
- / 可以定位到首页
视图,首页支持文件没有变化,只是改变了位置。
5. pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.my_project</groupId>
<artifactId>spring-mvc-demo-jetty</artifactId>
<version>0.1.0</version>
<properties>
<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
<jetty.version>9.3.7.v20160115</jetty.version>
<springframework.version>4.3.3.RELEASE</springframework.version>
<thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${springframework.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring4</artifactId>
<version>${thymeleaf.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>${jetty.version}</version>
</plugin>
</plugins>
</build>
</project>
这个程序最大的缺点就是你必须知道 Java 包之间的依赖,并明白依赖的包是否在运行时需要或仅编译时需要。这对入门者非常不友好!!!
6.运行
运行命令:
mvn jetty:run
服务器也启动在 http://localhost:8080 端口。
四、无 XML 配置实现 spring4 mvc 使用 jetty
从 spring3 开始,Spring 实现了无 xml 配置,包括对 servlet 3+ 规范对 WebContext (web.xml)可编程支持。对 Spring3+ mvc 的核心接口是 WebApplicationInitializer 。实现该接口,就可以直接启动 web 应用,并进行 WebContext 的配置。一般继承该接口的 AbstractDispatcherServletInitializer 实现类,配置 web context。
1. 程序结构
这个程序非常类似 spring-boot 的结构,仅是多了 config 包下两个 Java 类
2. 配置类(Java-based Configuration Class)
SpringServletInitializer 其实就是 web.xml 的 Java 形式。这里仅是比前面多了常用的 UTF-8 汉字支持的过滤器。
public class SpringServletInitializer extends AbstractDispatcherServletInitializer {
public static final String CHARACTER_ENCODING = "UTF-8";
public SpringServletInitializer() {
super();
}
protected WebApplicationContext createServletApplicationContext() {
final AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(SpringWebConfig.class);
// context.register(PersistenceJPAConfig.class);
return context;
}
protected WebApplicationContext createRootApplicationContext() {
return null;
}
protected String[] getServletMappings() {
return new String[] { "/" };
}
@Override
protected Filter[] getServletFilters() {
final CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
encodingFilter.setEncoding(CHARACTER_ENCODING);
encodingFilter.setForceEncoding(true);
return new Filter[] { encodingFilter };
}
}
SpringWebConfig 类就是 hello-servlet.xml 配置形式
@Configuration
@EnableWebMvc
@ComponentScan("hello")
public class SpringWebConfig
extends WebMvcConfigurerAdapter implements ApplicationContextAware {
private ApplicationContext applicationContext;
public SpringWebConfig() {
super();
}
public void setApplicationContext(final ApplicationContext applicationContext)
throws BeansException {
this.applicationContext = applicationContext;
}
/* ******************************************************************* */
/* GENERAL CONFIGURATION ARTIFACTS */
/* Static Resources, i18n Messages, Formatters (Conversion Service) */
/* ******************************************************************* */
/**
* Dispatcher configuration for serving static resources
*/
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
registry.addResourceHandler("/images/**").addResourceLocations("classpath:/static/images/");
registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/");
registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/");
registry.addResourceHandler("/**/*.html").addResourceLocations("classpath:/static/");
}
/* **************************************************************** */
/* THYMELEAF-SPECIFIC ARTIFACTS */
/* TemplateResolver <- TemplateEngine <- ViewResolver */
/* **************************************************************** */
@Bean
public SpringResourceTemplateResolver templateResolver(){
// SpringResourceTemplateResolver automatically integrates with Spring's own
// resource resolution infrastructure, which is highly recommended.
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setApplicationContext(this.applicationContext);
//templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setPrefix("classpath:/templates/");
templateResolver.setSuffix(".html");
// HTML is the default value, added here for the sake of clarity.
templateResolver.setTemplateMode(TemplateMode.HTML);
// Template cache is true by default. Set to false if you want
// templates to be automatically updated when modified.
templateResolver.setCacheable(true);
return templateResolver;
}
@Bean
public SpringTemplateEngine templateEngine(){
// SpringTemplateEngine automatically applies SpringStandardDialect and
// enables Spring's own MessageSource message resolution mechanisms.
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
// Enabling the SpringEL compiler with Spring 4.2.4 or newer can
// speed up execution in most scenarios, but might be incompatible
// with specific cases when expressions in one template are reused
// across different data types, so this flag is "false" by default
// for safer backwards compatibility.
templateEngine.setEnableSpringELCompiler(true);
return templateEngine;
}
@Bean
public ThymeleafViewResolver viewResolver(){
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
return viewResolver;
}
}
你可能以注意到其中一些 "classpath:" 。这时找静态文件就在 resource 目录下了。
3. 控制器
@Controller
public class GreetingController {
@RequestMapping("/greeting")
public String greeting(@RequestParam(value = "name", required = false, defaultValue = "World") String name,
Model model) {
model.addAttribute("name", name);
return "greeting";
}
@RequestMapping("/hello")
public void sayHello(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().println("<h1>Hello ,!!!!</h1>");
response.getWriter().println("session=" + request.getSession(true).getId());
}
// http://stackoverflow.com/questions/7415084/spring-welcome-file-list-correct-mapping
@RequestMapping("/")
public String root() {
return "redirect:/index.html";
}
}
你会发现这个程序与前面的控制器一样哦!
4. 运行
pom.xml 同 jetty 版本(XML 配置),资源文件没有变化。所以,运行程序依然是:
mvn jetty:run
服务器也启动在 http://localhost:8080 端口。
五、结论
本文给出了 Spring 官方 Hello world MVC 在 spring boot,jetty 容器中的实现。其中 jetty 中包含 xml 配置 和 无 xml 配置形式。总的感觉,没有常用案例支持,仅依靠官方文档(几乎都有说明,但难找到解决方案),不同形式的配置迁移是比较难配置和调试的。除了 Spring boot 应用, XML 与 java-based 配置几乎都是对应的。
- XML 配置版本:可以使用 spring 2 - 4 任意版本,部署到任意 servlet 容器中运行,网上中文资料最丰富。缺点,程序变大后配置灾难在所难免;
- 无 XML 配置版本:可以使用 spring 3 - 4 版本,部署到 servlet3+ 容器中运行,网上中文资料质量好坏难辨。非常合适以前的系统升级,也合适新手学习(配置类不同程序都类似,拷贝来就能用)
- Spring boot 应用版本: 只能使用 sping 4 和 spring boot,网上中文资料几乎为零。胜在入门容易,几乎零配置,与 Node.js python 等比也算简单的。
【参考】
[1] Web MVC framework http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html