SpringMVC简介
SpringMVC是当前最优秀的MVC框架,自从Spring 2.5版本发布后,由于支持注解配置,易用性有了大幅度的提高。Spring 3.0更加完善,实现了对Struts 2的超越。现在越来越多的开发团队选择了Spring MVC。
- Spring为展现层提供的基于MVC设计理念的优秀的Web框架,是目前最主流的MVC框架之一
- Spring3.0后全面超越Struts2,成为最优秀的MVC框架
- Spring MVC通过一套MVC注解,让POJO成为处理请求的控制器,而无须实现任何接口。
- 支持REST风格的URL请求
- 采用了松散耦合可插拔组件结构,比其他MVC框架更具扩展性和灵活性
迷你版的Spring MVC,我将在一个干净的web工程开始开发,不引入Spring,完全通过JDK来实现。
执行过程如图所示:
⑴用户发送请求至前端控制器DispatcherServlet。
⑵ DispatcherServlet收到请求调用HandlerMapping处理器映射器。
⑶ 处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
⑷ DispatcherServlet通过HandlerAdapter处理器适配器调用处理器。
⑸ 执行处理器(Controller,也叫后端控制器)。
⑹ Controller执行完成返回ModelAndView。
⑺ HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
⑻ DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
⑼ ViewReslover解析后返回具体View。
⑽ DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)。
⑾ DispatcherServlet响应用户。
从上面可以看出,DispatcherServlet有接收请求,响应结果,转发等作用。有了DispatcherServlet之后,可以减少组件之间的耦合度。
SpringMVC九大组件
HandlerMapping
是用来查找Handler的。在SpringMVC中会有很多请求,每个请求都需要一个Handler处理,具体接收到一个请求之后使用哪个Handler进行处理,这就是HandlerMapping需要做的事。
HandlerAdapter
从名字上看,它就是一个适配器。因为SpringMVC中的Handler可以是任意的形式,只要能处理请求就ok,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法。如何让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的事情。
小结:Handler是用来干活的工具;HandlerMapping用于根据需要干的活找到相应的工具;HandlerAdapter是使用工具干活的人。
HandlerExceptionResolver
其它组件都是用来干活的。在干活的过程中难免会出现问题,出问题后需要有一个专门的角色对异常情况进行处理,在SpringMVC中就是HandlerExceptionResolver。具体来说,此组件的作用是根据异常设置ModelAndView,之后再交给render方法进行渲染。
ViewResolver
ViewResolver用来将String类型的视图名和Locale解析为View类型的视图。View是用来渲染页面的,也就是将程序返回的参数填入模板里,生成html(也可能是其它类型)文件。这里就有两个关键问题:使用哪个模板?用什么技术(规则)填入参数?这其实是ViewResolver主要要做的工作,ViewResolver需要找到渲染所用的模板和所用的技术(也就是视图的类型)进行渲染,具体的渲染过程则交由不同的视图自己完成。
RequestToViewNameTranslator
ViewName是根据ViewName查找View,但有的Handler处理完后并没有设置View也没有设置ViewName,这时就需要从request获取ViewName了,如何从request中获取ViewName就是RequestToViewNameTranslator要做的事情了。RequestToViewNameTranslator在Spring MVC容器里只可以配置一个,所以所有request到ViewName的转换规则都要在一个Translator里面全部实现。
LocaleResolver
解析视图需要两个参数:一是视图名,另一个是Locale。视图名是处理器返回的,Locale是从哪里来的?这就是LocaleResolver要做的事情。LocaleResolver用于从request解析出Locale,Locale就是zh-cn之类,表示一个区域,有了这个就可以对不同区域的用户显示不同的结果。SpringMVC主要有两个地方用到了Locale:一是ViewResolver视图解析的时候;二是用到国际化资源或者主题的时候。
ThemeResolver
用于解析主题。SpringMVC中一个主题对应一个properties文件,里面存放着跟当前主题相关的所有资源、如图片、css样式等。SpringMVC的主题也支持国际化,同一个主题不同区域也可以显示不同的风格。SpringMVC中跟主题相关的类有 ThemeResolver、ThemeSource和Theme。主题是通过一系列资源来具体体现的,要得到一个主题的资源,首先要得到资源的名称,这是ThemeResolver的工作。然后通过主题名称找到对应的主题(可以理解为一个配置)文件,这是ThemeSource的工作。最后从主题中获取资源就可以了。
MultipartResolver
用于处理上传请求。处理方法是将普通的request包装成MultipartHttpServletRequest,后者可以直接调用getFile方法获取File,如果上传多个文件,还可以调用getFileMap得到FileName->File结构的Map。此组件中一共有三个方法,作用分别是判断是不是上传请求,将request包装成MultipartHttpServletRequest、处理完后清理上传过程中产生的临时资源。
FlashMapManager
用来管理FlashMap的,FlashMap主要用在redirect中传递参数。
下面直接上已完成的代码!
工程目录结构
第一,在annotation包下,我将提供自定义的注解,为了方便理解,就和Spring MVC保持一致。
第二,为了模拟Spring MVC的方法调用链,我这里提供Controller/Service/Dao层进行测试
第三,提供自己的DispatcherServlet完成核心处理逻辑
关于自定义注解
JDK提供了几个元注解,比如:
@Documented : JavaDoc文档
@Target:标志此注解可以修饰在哪些地方,类,成员变量,方法...
@Retention:Annotation的生命周期,一般情况下,我们自定义注解的话,显然需要在运行期获取注解的一些信息。
@Controller注解
模拟Spring MVC的@Controller注解
package com.mymvc.annotation;
import java.lang.annotation.*;
/**
* 控制层注解
*/
@Documented // JAVADOC
@Target(ElementType.TYPE) // 作用于类上
@Retention(RetentionPolicy.RUNTIME) // 限制Annotation的生命周期,需要运行时保留
public @interface Controller {
/**
* 作用于该类上的注解有一个value属性,就是controller的名称
*/
public String value();
}
@Qualifier提供依赖注入
package com.mymvc.annotation;
import java.lang.annotation.*;
/**
*
*/
@Documented
@Target(ElementType.FIELD) // 作用于字段上,实现注入
@Retention(RetentionPolicy.RUNTIME)
public @interface Qualifier {
public String value();
}
@RequestMapping提供URL地址处理映射
package com.mymvc.annotation;
import java.lang.annotation.*;
/**
* 地址映射处理注解
*/
@Documented
@Target({ElementType.METHOD, ElementType.TYPE}) // 该注解可用于类以及方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
public String value();
}
@Repository Dao层注解
package com.mymvc.annotation;
import java.lang.annotation.*;
/**
* 持久化层注解
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Repository {
public String value();
}
@Service 层注解
package com.mymvc.annotation;
import java.lang.annotation.*;
/**
* 业务层注解
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
public String value();
}
DispatcherServlet (编写核心控制器)
在Spring MVC中,DispatcherServlet是核心,下面我们来实现它。首先来说,Spring MVC中的DispatcherServlet说到底,还是HttpServlet的子类,因此我这边自己的DispatcherSerlvet需要extends HttpServlet。
先添加pom依赖,提供servlet依赖
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
DispatcherServlet
package com.mymvc.servlet;
import com.mymvc.annotation.*;
import com.mymvc.controller.UserController;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@WebServlet(name = "dispatcherServlet", urlPatterns = "/*", loadOnStartup = 1, initParams = {
@WebInitParam(name = "base-package", value = "com.mymvc") })
public class DispatcherServlet extends HttpServlet {
// 扫描的基包
private String basePackage = "";
// 基包下面所有的带包路径权限定类名
private List<String> packageNames = new ArrayList<String>();
// 注解实例化,注解上的名称:实例化对象
private Map<String, Object> instanceMap = new HashMap<String, Object>();
// 带包路径的权限定名称:注解上的名称
private Map<String, String> nameMap = new HashMap<String, String>();
// URL地址和方法的映射关系,SpringMvc就是方法的调用链
private Map<String, Method> urlMethodMap = new HashMap<String, Method>();
// Method和权限定类型映射关系,主要是为了通过Method找到该方法的对象利用反射执行
private Map<Method, String> methodPackageMap = new HashMap<Method, String>();
/**
* 初始化
*
* @param config
* @throws ServletException
*/
@Override
public void init(ServletConfig config) throws ServletException {
basePackage = config.getInitParameter("base-package");
try {
// 1.扫描基包得到全部的带包路径权限定名
scanBasePackage(basePackage);
// 2.把带有@Controller/@Service/@Repository的类实例化放入map中,key为注解的名称
instance(packageNames);
// 3.Spring IOC 注入
SpringIOC();
// 4.完成URL地址与发放的映射关系
handlerUrlMethodMap();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
private void scanBasePackage(String basePackage) {
// 注意为了得到基包下面的url路径,需要对basepackage做转换:将.替换为/
URL url = this.getClass().getClassLoader().getResource(basePackage.replaceAll("\\.", "/"));
File basePackageFile = new File(url.getPath());
System.out.println("scan:" + basePackageFile);
File[] childFiles = basePackageFile.listFiles();
for (File file : childFiles) {
if (file.isDirectory()) { // 目录继续递归扫描
scanBasePackage(basePackage + "." + file.getName());
} else if (file.isFile()) {
// 类似这种:com.mymvc.service.impl.UserServiceImpl.class 去掉class
packageNames.add(basePackage + "." + file.getName().split("\\.")[0]);
}
}
}
private void instance(List<String> packageNames)
throws ClassNotFoundException, InstantiationException, IllegalAccessException {
if (packageNames.size() < 1) {
return;
}
for (String string : packageNames) {
Class c = Class.forName(string);
if (c.isAnnotationPresent(Controller.class)) {
Controller controller = (Controller) c.getAnnotation(Controller.class);
String controllerName = controller.value();
instanceMap.put(controllerName, c.newInstance());
nameMap.put(string, controllerName);
System.out.println("Controller : " + string + " , value : " + controller.value());
} else if (c.isAnnotationPresent(Service.class)) {
Service service = (Service) c.getAnnotation(Service.class);
String serviceName = service.value();
instanceMap.put(serviceName, c.newInstance());
nameMap.put(string, serviceName);
System.out.println("Service : " + string + " , value : " + service.value());
} else if (c.isAnnotationPresent(Repository.class)) {
Repository repository = (Repository) c.getAnnotation(Repository.class);
String repositoryName = repository.value();
instanceMap.put(repositoryName, c.newInstance());
nameMap.put(string, repositoryName);
System.out.println("Controller : " + string + " , value : " + repository.value());
}
}
}
private void SpringIOC() throws ClassNotFoundException, IllegalAccessException {
for (Map.Entry<String, Object> entry : instanceMap.entrySet()) {
Field[] fields = entry.getValue().getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Qualifier.class)) {
String name = field.getAnnotation(Qualifier.class).value();
field.setAccessible(true);
field.set(entry.getValue(), instanceMap.get(name));
}
}
}
}
private void handlerUrlMethodMap() throws ClassNotFoundException {
if (packageNames.size() < 1) {
return;
}
for (String string : packageNames) {
Class c = Class.forName(string);
if (c.isAnnotationPresent(Controller.class)) {
Method[] methods = c.getMethods();
StringBuffer baseUrl = new StringBuffer();
if (c.isAnnotationPresent(RequestMapping.class)) {
RequestMapping requestMapping = (RequestMapping) c.getAnnotation(RequestMapping.class);
baseUrl.append(requestMapping.value());
}
for (Method method : methods) {
if (method.isAnnotationPresent(RequestMapping.class)) {
RequestMapping requestMapping = (RequestMapping) method.getAnnotation(RequestMapping.class);
baseUrl.append(requestMapping.value());
urlMethodMap.put(baseUrl.toString(), method);
methodPackageMap.put(method, string);
}
}
}
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String uri = req.getRequestURI();
String contextPath = req.getContextPath();
String path = uri.replaceAll(contextPath, "");
// 通过path找到method
Method method = urlMethodMap.get(path);
if (method != null) {
// 通过method拿到controller对象,准备反射执行
String packageName = methodPackageMap.get(method);
String controllerName = nameMap.get(packageName);
// 拿到controller对象
UserController userController = (UserController) instanceMap.get(controllerName);
try {
method.setAccessible(true);
method.invoke(userController);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
DispatcherServlet
@WebServlet是什么?
其实,以前我们定义一个Servlet,需要在web.xml中去配置,不过在Servlet3.0后出现了基于注解的Servlet。
仔细观察,你会发现,这个DispatcherServlet是自启动,而且传入了一个参数。
要知道,在Spring MVC中,要想基于注解,需要在配置中指明扫描的包路径,就像这个样子:
<context:component-scan base-package=“com.zfz.myspringmvc”>
</context:component-scan>
为了方便,我这里就通过初始化参数直接将需要扫描的基包路径传入。
init()
其实,在init中,我们主要是完成了什么呢?
第一,我们应该去扫描基包下的类,得到信息A
第二,对于@Controller/@Service/@Repository注解而言,我们需要拿到对应的名称,并初始化它们修饰的类,形成映射关系B
第三,我们还得扫描类中的字段,如果发现有@Qualifier的话,我们需要完成注入
第四,我们还需要扫描@RequestMapping,完成URL到某一个Controller的某一个方法上的映射关系C
其实,Spring MVC的处理流程,就是类似这样的!
扫描基包(scanBasePackage)
注意,基包是X.Y.Z的形式,而URL是X/Y/Z的形式,需要转换。
实例化(instance)
从这里你可以看出,我们完成了被注解标注的类的实例化,以及和注解名称的映射。
依赖注入(SpringIOC)
以前,我们总是说Spring IOC,上面不就是在做这个事情么?
URL映射处理(handlerUrlMethodMap)
URL,我们需要提取出来,映射到Controller的Method上。
doGet/doPost
在doPost方法中,非常简单,我们只需要提取出URL,通过URL映射到Method上,然后通过反射的方式进行调用即可。
Run
controller
package com.mymvc.controller;
import com.mymvc.annotation.Controller;
import com.mymvc.annotation.Qualifier;
import com.mymvc.annotation.RequestMapping;
import com.mymvc.service.UserService;
@Controller("userController")
@RequestMapping("/user")
public class UserController {
@Qualifier("userServiceImpl")
private UserService userService;
@RequestMapping("/insert")
public void insert() {
userService.insert();
}
}
service
package com.mymvc.service;
public interface UserService {
public void insert();
}
serviceImpl
package com.mymvc.service;
import com.mymvc.annotation.Qualifier;
import com.mymvc.annotation.Service;
import com.mymvc.dto.mapper.UserMapper;
@Service("userServiceImpl")
public class UserServiceImpl implements UserService {
@Qualifier("userMapperImpl")
private UserMapper userMapper;
@Override
public void insert() {
System.out.println("UserServiceImpl.insert() start");
userMapper.insert();
System.out.println("UserServiceImpl.insert() end");
}
}
mapper
package com.mymvc.dto.mapper;
public interface UserMapper {
public void insert();
}
mapperImpl
package com.mymvc.dto.mapper;
import com.mymvc.annotation.Repository;
@Repository("userMapperImpl")
public class UserMapperImpl implements UserMapper {
@Override
public void insert() {
System.out.println("execute UserMapperImpl.insert()");
}
}
运行结果
http://localhost:8080/user/insert
供参考!