1 搭建 SpringMVC 底层机制开发环境
1.1 创建 Maven 项目 my-springmvc

1.2 对 my-springmvc 进行配置: 创建 test 资源目录(直接复制main的资源目录即可)



1.3 在pom文件中添加基本的依赖
<dependencies>
<!--引入junit测试依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--引入原生servlet依赖 的jar-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<!--
1. scope标签表示引入的jar的作用范围
2. provided:表示该项目在打包,放到生产环境的时候,不需要带上servlet-api.jar
3. 因为tomcat本身是有servlet的jar, 到时直接使用tomcat本身的servlet-api.jar,防止版本冲突
-->
<scope>provided</scope>
</dependency>
<!--引入dom4j,解析xml文件-->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<!--引入常用工具类的jar 该jar含有很多常用的类-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.5</version>
</dependency>
</dependencies>
1.4 添加 Web 应用程序
注意:这里还需要对web目录配置资源路径,可参考文章http://t.csdnimg.cn/750uh


1.5 到此: 项目开发环境搭建完成
2 实现 SpringMVC 底层机制
2.1 阶段 1- 开发 MyDispatcherServlet 核心控制分发器
说明: 编写 MyDispatcherServlet 充当原生的 DispatcherServlet(即核心控制器)
需要实现的功能分析:
- 拦截浏览器发来的请求
代码实现
(1)在java目录下创建软件包 com.myspringmvc.servlet,在该包下创建类 MyDispatcherServlet
package com.myspringmvc.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 1. MyDispatcherServlet充当原生DispatcherServlet
* 2. 本质是一个Servlet, 继承HttpServlet
*/
public class MyDispatcherServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
System.out.println("--MyDispatcherServlet---doGet--");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
System.out.println("--MyDispatcherServlet---doPost--");
}
}
(2)在main/resource 目录下创建 myspringmvc.xml, 充当原生的 applicationContext-mvc.xml 文件(就是 spring 的容器配置文件), 先创建好一个空的文件

(3)修改 web\WEB-INF\web.xml, 完成 MyDispatcherServlet 前端控制器的配置
<!--配置MyDispatcherServlet,作为我们自己的前端控制器-->
<servlet>
<servlet-name>myDispatcherServlet</servlet-name>
<servlet-class>com.myspringmvc.servlet.MyDispatcherServlet</servlet-class>
<!--配置属性contextConfigLocation,指定 DispatcherServlet 去操作的spring配置文件-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:myspringmvc.xml</param-value>
</init-param>
<!--在web项目启动时,就自动加载MyDispatcherServlet-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>myDispatcherServlet</servlet-name>
<!--因为MyDispatcherServlet作为前端控制器,所以需要拦截所有请求,url-pattern 配置 /-->
<url-pattern>/</url-pattern>
</servlet-mapping>
(4)配置 Tomcat 服务器




(5)启动服务器,输入网址 http://localhost:8080/springmvc/abc,控制有如下输出,说明我们的前端控制器配置成功,即发出的请求成功被我们自己的控制器拦截

2.2 阶段 2- 完成客户端/浏览器可以请求控制层
2.2.1 创建 自定义注解和 自己的 Controller
(1)在包 myspringmvc 下创建软件包 annotation,在 annotation 下创建注解 @Controller 和 @RequestMapping
package com.myspringmvc.annotation;
import java.lang.annotation.*;
/**
* 该注解用于标识一个控制器组件
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
String value() default "";
}
package com.myspringmvc.annotation;
import java.lang.annotation.*;
/**
* 该注解用于指定控制器某个方法的映射路径
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
String value() default "";
}
(2)在包 com 下创建软件包 controller,在controller下创建java类 MonsterController.java
package com.controller;
import com.myspringmvc.annotation.Controller;
import com.myspringmvc.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Controller
public class MonsterController {
// 编写方法,可以列出妖怪列表
//springmvc 是支持原生的servlet api的,为了看到底层机制
//这里设计两个参数
@RequestMapping("/list/monster")
public void listMonster(HttpServletRequest request, HttpServletResponse response){
// 设置编码和返回类型
response.setContentType("text/html;charset=utf-8");
// 获取 writer 返回信息
try {
PrintWriter printWriter = response.getWriter();
printWriter.write("<h1>妖怪列表信息</h1>");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
2.2.2 配置 myspringmvc.xml
<?xml version="1.0" encoding="utf-8" ?>
<beans>
<!--指定要扫描的基本包以及子包的类-->
<component-scan base-package="com.controller"></component-scan>
</beans>
2.2.3 编写 XMLParser 工具类,用于解析 myspringmvc.xml
(1)在包 myspringmvc 创建子包 xml,在 xml 下创建Java类 XMLParse.java
package com.myspringmvc.xml;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
/**
* 该类用于解析spring配置文件
*/
public class XMlParser {
// 解析xml文件,得到要扫描的包
public static String getBasePackage(String xmlFile){
SAXReader saxReader = new SAXReader();
//通过得到类的加载路径,获取到容器配置文件(对应的资源流)
ClassLoader classLoader = XMlParser.class.getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream(xmlFile);
try {
// 得到xmlFile文档
Document document = saxReader.read(inputStream);
Element rootElement = document.getRootElement();
// 得到 component-scan 元素
Element componentScanElement = rootElement.element("component-scan");
// 得到 base-package 属性
Attribute attribute = componentScanElement.attribute("base-package");
// 得到 base-package 的属性值
String basePackage = attribute.getText();
return basePackage;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
(2)测试该类,在 test/java 目录下创建软件包 com.test,在该包下创建测试类 MySpringMVCTest.java
package com.test;
import com.myspringmvc.xml.XMlParser;
import org.junit.Test;
public class MySpringMVCTest {
@Test
public void readXML(){
// 这里注意一定要引入自己写的 XMlParser,import com.myspringmvc.xml.XMlParser;
String basePackage = XMlParser.getBasePackage("myspringmvc.xml");
System.out.println("basePackage=" + basePackage);
}
}
(3)测试结果如下,成功拿到要扫描的包

2.2.4 开发 MyWebApplicationContext,充当 Spring 容器-得到扫描类的全路径列表
功能说明:把指定的目录包括子目录下的 java 类的全路径扫描到集合中,
预计效果如下:
扫描后的 classFullPathList= [com.controller.MonsterController,
com.service.impl.MonsterServiceImpl, com.service.MonsterService]
(1)在包 myspringmvc 创建子包 context,在 context 下创建Java类 MyWebApplicationContext.java
package com.myspringmvc.context;
import com.myspringmvc.xml.XMLParser;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/**
* 该类作为我们自己的spring容器
*/
public class MyWebApplicationContext {
//保存扫描包/子包的类的全路径
private List<String> classFullPath = new ArrayList<>();
// 该方法完成自己spring容器的初始化
public void init(){
// 得到要扫描的包
String basePackage = XMLParser.getBasePackage("myspringmvc.xml");
String[] basePackages = basePackage.split(",");
if (basePackages.length > 0){
for (String pack : basePackages) {
//扫描
scanPackage(pack);
}
}
System.out.println("扫描后的 classFullPath = " + classFullPath);
}
/**
* 该方法完成对包的扫描
* @param pack 表示要扫描的包,比如"com.controller"
*/
public void scanPackage(String pack){
// 通过类加载器,得到包所在的工作路径(绝对路径)
ClassLoader classLoader = MyWebApplicationContext.class.getClassLoader();
// 这里需要对pack进行转换,因为 getResource 的参数为路径
//说明: 1. 这里不能直接使用Junit测试, 否则 url null
// 2. 需要启动tomcat来测试
URL url = classLoader.getResource(pack.replace(".", "/"));
// 根据得到的路径,对其进行扫描,把类的全路径,保存到classFullPath
String path = url.getFile();
File dir = new File(path);
for (File f : dir.listFiles()) {
if (f.isDirectory()){//如果是一个目录,进行递归扫描
scanPackage(pack+"."+f.getName());
} else if (f.isFile()) {
classFullPath.add(pack + "." + f.getName().substring(0,f.getName().lastIndexOf(".class")));
}
}
}
}
(2)修改类 DispatcherServlet.java , 增加方法 init(该方法会在服务器启动时自动调用)
@Override
public void init() throws ServletException {
MyWebApplicationContext ioc = new MyWebApplicationContext();
ioc.init();
}
(3)启动tomcat服务器,测试效果。如在控制台输出如下,测试成功
创建包service,类MonsterService用于测试
在 myspringmvc.xml 文件要扫描的包中增加该包
<component-scan base-package="com.controller,com.service"></component-scan>

注意:这里如果报错java.lang.NoClassDefFoundError: org/dom4j/io/SAXReader,可参考文章 http://t.csdnimg.cn/YTwUd
2.2.5 完善 MyWebApplicationContext,充当 Spring 容器-实例化对象到容器中
功能说明:将扫描到的类, 在满足条件的情况下(即有相应的注解@Controller @Service...), 反射注 入到 ioc 容器
(1)修改java类 MyWebApplicationContext.java
- 增加属性 ioc
//属性ioc,存放反射生成的Bean对象
public ConcurrentHashMap<String, Object> ioc = new ConcurrentHashMap<>();
- 增加方法 executeInstance
//该方法将扫描到的类,在满足条件的情况下,反射到ioc容器
public void executeInstance(){
// 判断是否扫描到类
if (classFullPath.size() == 0){//说明没有扫描到类
return;
}
// 遍历 classFullPath,进行反射
for (String fullPath : classFullPath) {
Class<?> aClass = null;
try {
// 得到class 对象
aClass = Class.forName(fullPath);
// 判断是否存在 @Controller 注解
if (aClass.isAnnotationPresent(Controller.class)) {
// 得到 @Controller 注解
Controller controller = aClass.getDeclaredAnnotation(Controller.class);
// 得到value值
String value = controller.value();
String beanName = value;
// 如果没有指定value值,则默认为类名首字母小写
if ("".equals(value)){
// 得到类名
String className = aClass.getSimpleName();
// 将字符串首字母小写
beanName = StringUtils.uncapitalize(className);
}
Object instance = aClass.newInstance();
ioc.put(beanName, instance);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
- 修改 init 方法,在该方法中调用executeInstance
// 该方法完成自己spring容器的初始化
public void init(){
// 得到要扫描的包
String basePackage = XMLParser.getBasePackage("myspringmvc.xml");
String[] basePackages = basePackage.split(",");
if (basePackages.length > 0){
for (String pack : basePackages) {
//扫描,得到要扫描包的类全路径
scanPackage(pack);
}
}
System.out.println("扫描后的 classFullPath = " + classFullPath);
// 将满足条件的类,创建对象,放入ioc容器中
executeInstance();
System.out.println("扫描后的 ioc = "+ioc);
}
(3)启动tomcat服务器,测试效果。如在控制台输出如下,测试成功

2.5.6 完成请求 URL 和控制器方法的映射关系
功能说明:将配置的@RequestMapping 的 url 和 对应的 控制器、方法 的映射关系保存到集合中
预计效果如下:
handlerList= [MyHandler{
url="/monster/list",
controller=com.hspedu.controller.MonsterController@714dab1,
method=public void com.hspedu.controller.MonsterController.
listMonsters(javax.servlet.http.HttpServletRequest,
javax.servlet.http.HttpServletResponse)}]
(1)在myspringmvc 包下,新建一个包 handler,在handler下创建java类 MyHandler
package com.myspringmvc.handler;
import java.lang.reflect.Method;
/**
* 该类记录请求的 url 和 控制器方法映射关系
*/
public class MyHandler {
private String url;
private Object controller;
private Method method;
public MyHandler(String url, Object controller, Method method) {
this.url = url;
this.controller = controller;
this.method = method;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Object getController() {
return controller;
}
public void setController(Object controller) {
this.controller = controller;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
@Override
public String toString() {
return "MyHandler{" +
"\nurl='" + url + '\'' +
", \ncontroller=" + controller +
", \nmethod=" + method +
'}';
}
}
(2)修改类 DispatcherServlet.java , 增加属性handlerList,增加方法 initHandlerMapping,作为控制器映射器
private List<MyHandler> handlerList = new ArrayList<>();
/**
* 该方法作为一个控制器映射器,完成对 url 和 控制器方法 的映射 的初始化
*/
private void initHandlerMapping(ConcurrentHashMap<String, Object> ioc){
if (ioc.isEmpty()){
// 判断当前ioc容器是否为null
return;
}
// 遍历容器
for (String s : ioc.keySet()) {
// 获取到控制器对象
Object o = ioc.get(s);
// 获取到class对象
Class<?> aClass = o.getClass();
// 判断该对象是否为一个控制器
if (aClass.isAnnotationPresent(Controller.class)){// 得到该对象的所有方法
Method[] methods = aClass.getDeclaredMethods();
// 遍历方法
for (Method method : methods) {
// 判断该方法是否有注解 @RequestMapping
if (method.isAnnotationPresent(RequestMapping.class)) {
// 取出 @RequestMapping
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
// 创建 MyHandler 对象,存放到集合中
handlerList.add(new MyHandler(requestMapping.value(), o, method));
}
}
}
}
}
(3)修改 init 方法,在该方法中调用 initHandlerMapping
@Override
public void init() throws ServletException {
MyWebApplicationContext applicationContext = new MyWebApplicationContext();
applicationContext.init();
initHandlerMapping(applicationContext.ioc);
System.out.println("handlerList初始化的结果 = " + handlerList);
}
(4)启动tomcat服务器,测试效果。如在控制台输出如下,测试成功

2.5.7 完成 MyDispatcherServlet 分发请求到对应控制器方法
功能说明:当用户发出请求,根据用户请求 url 找到对应的控制器-方法, 并反射调用
(1) 修改类 DispatcherServlet.java,增加方法getMyHandler 和 executeDispatch
// 编写方法,通过request对象,返回MyHandler对象
// 如果没有,就返回null
private MyHandler getMyHandler(HttpServletRequest request){
// 判断handlerList是否为null
if (handlerList.isEmpty()) return null;
// 先获取到用户请求的 uri,比如http://localhost:8080/springmvc/monster/list
// uri = /springmvc/monster/list
String requestURI = request.getRequestURI();
// 获取到工程路径 /springmvc
String contextPath = request.getContextPath();
// 从uri中去除掉工程路径,得到 url = /monster/list
String url = requestURI.substring(contextPath.length());
// 遍历 handlerList
for (MyHandler handler : handlerList) {
if (handler.getUrl().equals(url)){
return handler;
}
}
return null;
}
// 编写方法,分发请求
private void executeDispatch(HttpServletRequest request, HttpServletResponse response){
MyHandler myHandler = getMyHandler(request);
try {
if (myHandler == null) {
response.getWriter().print("<h1>404 NOT FOUNT</h1>");
}else {
// 通过反射调用该方法
myHandler.getMethod().invoke(myHandler.getController(),request, response);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
(2)修改 doGet 和 doPost 方法,使前端控制器在拦截请求后,调用executeDispatch进行请求的分发
@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 {
// 分发请求
executeDispatch(req, resp);
}
(3)启动tomcat服务器,测试效果。在地址栏输入 http://localhost:8080/springmvc/list/monster 显示如下页面,测试成功

2.3 阶段 3- 从 web.xml 动态获取 myspringmvc.xml
功能说明:前面我们加载 myspringmvc.xml 是硬编码, 现在做活, 从 web.xml 动态获取
(1)修改类 DispatcherServlet.java 中的 init 方法,读取容器配置文件名
@Override
public void init(ServletConfig servletConfig) throws ServletException {
// 获取到web.xml文件中配置的spring容器配置文件名
String configLocation = servletConfig.getInitParameter("contextConfigLocation");
// 创建自己的 spring 容器
MyWebApplicationContext applicationContext = new MyWebApplicationContext(configLocation);
// 初始化容器
applicationContext.init();
// 把url 映射到 控制器的方法
initHandlerMapping(applicationContext.ioc);
System.out.println("handlerList初始化的结果 = " + handlerList);
}
(2)修改类 MyWebApplicationContext.java
- 增加属性 configLocation,增加有参和无参构造器
/ 该属性存放配置文件路径
private String configLocation;
public MyWebApplicationContext(String configLocation) {
this.configLocation = configLocation;
}
public MyWebApplicationContext() {
}
修改init方法,替换spring配置文件路径
// 该方法完成自己spring容器的初始化
public void init(){
// 得到要扫描的包
String basePackage = XMLParser.getBasePackage(configLocation.split(":")[1]);
String[] basePackages = basePackage.split(",");
if (basePackages.length > 0){
for (String pack : basePackages) {
//扫描,得到要扫描包的类全路径
scanPackage(pack);
}
}
System.out.println("扫描后的 classFullPath = " + classFullPath);
// 将满足条件的类,创建对象,放入ioc容器中
executeInstance();
System.out.println("扫描后的 ioc = "+ioc);
}
(3)启动tomcat服务器,测试效果。在地址栏输入 http://localhost:8080/springmvc/list/monster 显示如下页面,测试成功

2.4 阶段 4- 完成自定义@Service 注解功能
功能说明: 如果给某个类加上@Service, 则可以将其注入到我们的 Spring 容器
(1)在包myspringmvc.annotation下 ,创建注解 @Service
package com.myspringmvc.annotation;
import java.lang.annotation.*;
/**
* 该注解用于标识一个Service对象
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
String value() default "";
}
(2)在包com下,新建一个包service,在该包下创建接口MonsterService.java
package com.service;
import com.entity.Monster;
import java.util.List;
public interface MonsterService {
List<Monster> listMonster();
}
(3)在service包下,创建Java类 MonsterServiceImpl.java
package com.service.impl;
import com.entity.Monster;
import com.myspringmvc.annotation.Service;
import com.service.MonsterService;
import java.util.ArrayList;
import java.util.List;
/**
* 该类作为一个service注入到spring容器中
*/
@Service
public class MonsterServiceImpl implements MonsterService {
@Override
public List<Monster> listMonster() {
// 这里模拟数据
List<Monster> monsters = new ArrayList<>();
monsters.add(new Monster(100, "牛魔王", "芭蕉扇", 400));
monsters.add(new Monster(200, "老猫妖怪", "抓老鼠", 200));
return monsters;
}
}
(4)修改类 MyWebApplicationContext.java 中的 executeInstance 方法的部分代码
// 得到class 对象
aClass = Class.forName(fullPath);
// 判断是否存在 @Controller 注解 和 @Service
if (aClass.isAnnotationPresent(Controller.class) ||
aClass.isAnnotationPresent(Service.class)) {
String value = "";
if (aClass.isAnnotationPresent(Controller.class)){
// 得到 @Controller 注解
Controller controller = aClass.getDeclaredAnnotation(Controller.class);
// 得到value值
value = controller.value();
} else if (aClass.isAnnotationPresent(Service.class)) {
Service service = aClass.getDeclaredAnnotation(Service.class);
value = service.value();
}
String beanName = value;
// 如果没有指定value值,则默认为类名首字母小写
if ("".equals(value)){
// 得到类名
String className = aClass.getSimpleName();
// 将字符串首字母小写
beanName = StringUtils.uncapitalize(className);
}
Object instance = aClass.newInstance();
ioc.put(beanName, instance);
}
(5)启动tomcat服务器,测试效果。如在控制台输出如下,测试成功

2.5 阶段 5- 完成 Spring 容器对象的自动装配 -@Autowried
功能说明:完成 Spring 容器中对象的注入/自动装配
(1)在包 annotation 下新建注解 @Autowired
package com.myspringmvc.annotation;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
String value() default "";
}
(2)修改类 MyWebApplicationContext.java
- 增加方法 executeAutowired
// 该方法完成对象属性的装配,即依赖注入
private void executeAutowired(){
if (ioc.isEmpty()){ // 如果容器为空,抛出异常
throw new RuntimeException("ioc容器中,不存在你要装配的bean");
}
// 遍历ioc容器
for (String key : ioc.keySet()) {
// 得到对象
Object o = ioc.get(key);
// 得到class对象
Class<?> aClass = o.getClass();
// 得到所有属性
Field[] fields = aClass.getDeclaredFields();
// 遍历所有属性
for (Field field : fields) {
// 判断属性中是否存在Autowired注解
if (field.isAnnotationPresent(Autowired.class)){
// 得到指定的value值
String beanName = field.getAnnotation(Autowired.class).value();
if ("".equals(beanName)){
// 如果没有指定value值,则默认为属性名
beanName = field.getName();
}
// 从ioc容器中获取到对象
Object bean = ioc.get(beanName);
// 如果没有找到就抛出异常
if (bean == null){
throw new RuntimeException("ioc容器中,不存在你要装配的bean");
}
try {
// 因为该属性是私有的,所以需要进行爆破处理
field.setAccessible(true);
// 依赖注入
field.set(o, bean);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}
}
- 修改方法init,让其调用executeAutowired
// 该方法完成自己spring容器的初始化
public void init(){
// 得到要扫描的包
String basePackage = XMLParser.getBasePackage(configLocation.split(":")[1]);
String[] basePackages = basePackage.split(",");
if (basePackages.length > 0){
for (String pack : basePackages) {
//扫描,得到要扫描包的类全路径
scanPackage(pack);
}
}
System.out.println("扫描后的 classFullPath = " + classFullPath);
// 将满足条件的类,创建对象,放入ioc容器中
executeInstance();
System.out.println("扫描后的 ioc = "+ioc);
// 完成对象属性的装配
executeAutowired();
}
(3)修改类 MonsterController.java
增加属性
@Autowired
private MonsterServiceImpl monsterServiceImpl;
修改方法 listMonster
// 编写方法,可以列出妖怪列表
//springmvc 是支持原生的servlet api的,为了看到底层机制
//这里设计两个参数
@RequestMapping("/list/monster")
public void listMonster(HttpServletRequest request, HttpServletResponse response){
StringBuilder content = new StringBuilder("<h1>妖怪列表信息</h1>");
//调用monsterService
List<Monster> monsters = monsterServiceImpl.listMonster();
content.append("<table border='1px' width='500px' style='border-collapse:collapse'>");
for (Monster monster : monsters) {
content.append("<tr><td>" + monster.getId()
+ "</td><td>" + monster.getName() + "</td><td>"
+ monster.getSkill() + "</td><td>"
+ monster.getAge() + "</td></tr>");
}
content.append("</table>");
// 设置编码和返回类型
response.setContentType("text/html;charset=utf-8");
// 获取 writer 返回信息
try {
PrintWriter printWriter = response.getWriter();
printWriter.write(content.toString());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
(4)启动tomcat服务器,测试效果。在地址栏输入 http://localhost:8080/springmvc/list/monster 显示如下页面,测试成功

2.6 阶段 6- 完成控制器方法获取参数-@RequestParam
功能说明:使用自定义@RequestParam 和 方法参数名获取参数
2.6.1 将方法的HttpServletRequest 和 HttpServletResponse 参数封装到参数数组,进行反射调用
(1)修改类 DispatcherServlet.java 中的 executeDispatch方法
// 编写方法,分发请求
private void executeDispatch(HttpServletRequest request, HttpServletResponse response){
MyHandler myHandler = getMyHandler(request);
try {
if (myHandler == null) {
response.getWriter().print("<h1>404 NOT FOUNT</h1>");
}else {
// 1 得到目标方法的所有参数信息(对应的数组)
Class<?>[] parameterTypes = myHandler.getMethod().getParameterTypes();
// 2 创建一个参数数组,存放方法实参,在后面反射调用目标方法时,使用这个数组传参
Object[] params = new Object[parameterTypes.length];
// 3 遍历 parameterTypes 形参数组,根据形参数组信息,将实参填充到实参数组
for (int i = 0; i < parameterTypes.length; i++) {
// 取出每一个形参
Class<?> parameterType = parameterTypes[i];
// 如果这个形参类型是HttpServletRequest,就将request填充到params
// 原生SpringMVC中,是按照类型进行匹配的,这里进行了简化,使用了名称进行匹配
if ("HttpServletRequest".equals(parameterType.getSimpleName())){
params[i] = request;
} else if ("HttpServletResponse".equals(parameterType.getSimpleName())) {
params[i] = response;
}
}
// 通过反射调用该方法,通过实参参数数组传入参数
myHandler.getMethod().invoke(myHandler.getController(),params);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
2.6.2 将方法参数中指定 @RequestParam 的参数封装到参数数组,进行反射调用
(1)修改service包下的MonsterService接口,增加如下方法
//增加方法,通过传入的name,返回monster列表
public List<Monster> findMonsterByName(String name);
(2)修改service.impl包的MonsterServiceImpl类,增加如下方法
@Override
public List<Monster> findMonsterByName(String name) {
//这里模拟数据->DB
List<Monster> monsters =
new ArrayList<>();
monsters.add(new Monster(100, "牛魔王", "芭蕉扇", 400));
monsters.add(new Monster(200, "老猫妖怪", "抓老鼠", 200));
monsters.add(new Monster(300, "大象精", "运木头", 100));
monsters.add(new Monster(400, "黄袍怪", "吐烟雾", 300));
monsters.add(new Monster(500, "白骨精", "美人计", 800));
//创建集合返回查询到的monster集合
List<Monster> findMonsters =
new ArrayList<>();
//遍历monsters,返回满足条件
for (Monster monster : monsters) {
if (monster.getName().contains(name)) {
findMonsters.add(monster);
}
}
return findMonsters;
}
(3)修改controller包中的MonsterController类,增加如下方法
//增加方法,通过name返回对应的monster集合
@RequestMapping(value = "/monster/find")
public void findMonsterByName(HttpServletRequest request,
HttpServletResponse response,
@RequestParam("name") String name) {
//设置编码和返回类型
response.setContentType("text/html;charset=utf-8");
System.out.println("--接收到的name---" + name);
StringBuilder content = new StringBuilder("<h1>妖怪列表信息</h1>");
//调用monsterService
List<Monster> monsters = monsterServiceImpl.findMonsterByName(name);
content.append("<table border='1px' width='400px' style='border-collapse:collapse'>");
for (Monster monster : monsters) {
content.append("<tr><td>" + monster.getId()
+ "</td><td>" + monster.getName() + "</td><td>"
+ monster.getSkill() + "</td><td>"
+ monster.getAge() + "</td></tr>");
}
content.append("</table>");
//获取writer返回信息
try {
PrintWriter printWriter = response.getWriter();
printWriter.write(content.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
(4)修改类 DispatcherServlet.java ,增加getIndexRequestParameterIndex方法,修改executeDispatch方法
// 编写方法,分发请求
private void executeDispatch(HttpServletRequest request, HttpServletResponse response){
MyHandler myHandler = getMyHandler(request);
try {
if (myHandler == null) {
response.getWriter().print("<h1>404 NOT FOUNT</h1>");
}else {
// 1 得到目标方法的所有参数信息(对应的数组)
Class<?>[] parameterTypes = myHandler.getMethod().getParameterTypes();
// 2 创建一个参数数组,存放方法实参,在后面反射调用目标方法时,使用这个数组传参
Object[] params = new Object[parameterTypes.length];
// 3 遍历 parameterTypes 形参数组,根据形参数组信息,将实参填充到实参数组
for (int i = 0; i < parameterTypes.length; i++) {
// 取出每一个形参
Class<?> parameterType = parameterTypes[i];
// 如果这个形参类型是HttpServletRequest,就将request填充到params
// 原生SpringMVC中,是按照类型进行匹配的,这里进行了简化,使用了名称进行匹配
if ("HttpServletRequest".equals(parameterType.getSimpleName())){
params[i] = request;
} else if ("HttpServletResponse".equals(parameterType.getSimpleName())) {
params[i] = response;
}
}
//将http请求参数封装到params数组中
//1.获取http请求的参数集合
//请求可能是:http://localhost:8080/monster/find?name=牛魔王&hobby=打篮球&hobby=喝酒
//2. 返回的Map<String,String[]> String:表示http请求的参数名
// String[]:表示http请求的参数值,为什么是数组
//因为请求可能是这样一种形式:http://localhost:8080/monster/find?name=牛魔王&hobby=打篮球&hobby=喝酒
Map<String, String[]> parameterMap = request.getParameterMap();
//3.遍历parameterMap 将请求参数,按照顺序填充到实参数组params
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
// key对应请求的参数名
String key = entry.getKey();
// value对应请求的参数值。这里做了简化,只考虑提交的参数是单值的情况
String value = entry.getValue()[0];
// 得到请求的参数对应的是第几个形参
int indexRequestParameterIndex = getIndexRequestParameterIndex(myHandler.getMethod(), key);
if (indexRequestParameterIndex != -1) {
params[indexRequestParameterIndex] = value;
}else {
// 说明并没有找到@RequestParam注解对应的参数,就会使用默认的机制进行配置
//一会再写
}
}
// 通过反射调用该方法,通过实参参数数组传入参数
myHandler.getMethod().invoke(myHandler.getController(),params);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @param method 目标方法
* @param name 请求的参数名
* @return 是目标方法的第几个形参
*/
private int getIndexRequestParameterIndex(Method method, String name) {
// 得到该方法的所有形参参数
Parameter[] parameters = method.getParameters();
// 遍历所有参数
for (int i = 0; i < parameters.length; i++) {
// 取出当前参数
Parameter parameter = parameters[i];
// 判断该参数是否有@RequestParam注解
if (parameter.isAnnotationPresent(RequestParam.class)){
//得到@RequestParam注解
RequestParam annotation = parameter.getAnnotation(RequestParam.class);
//得到@RequestParam注解的value值
String value = annotation.value();
//如果注解的value值和请求的参数名key相同,就将参数值填充进对应位置的实参数组
if (name.equals(value)){
return i;
}
}
}
// 如果没有匹配成功,就返回-1
return -1;
}
(4)启动tomcat服务器,测试效果。在地址栏输入 http://localhost:8080/springmvc/monster/find?name=怪 ,显示如下页面,测试成功
2.6.3 完成: 在方法参数没有指定 @RequestParam 时,按照默认参数名获取值, 进行反射调用
(1)修改类 DispatcherServlet.java ,增加getParameterIndex方法
说明:在默认情况下 parameter.getName() 得到的名字不是形参真正名字,而是 [arg0, arg1, arg2...], 这里要在pom文件中引入下面这个插件,使用java8特性,这样才能解决
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<encoding>utf-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
引入插件后,点击 maven 管理,clean 项目,在重启一下 tomcat ,插件才会生效

getParameterIndex方法:
/**
* @param method 目标方法
* @param name 请求的参数名
* @return 是目标方法的第几个形参(这里的形参是没有@RequestParam注解的形参)
*/
public int getParameterIndex(Method method, String name) {
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
//在默认情况下 parameter.getName() 得到的名字不是形参真正名字
//而是 [arg0, arg1, arg2...], 这里我们要引入一个插件,使用java8特性,这样才能解决
String paramName = parameter.getName();
if (name.equals(paramName)){
return i;
}
}
return -1;
}
(2)修改类 DispatcherServlet.java 中的 executeDispatch 方法中的部分代码
//3.遍历parameterMap 将请求参数,按照顺序填充到实参数组params
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
// key对应请求的参数名
String key = entry.getKey();
// value对应请求的参数值。这里做了简化,只考虑提交的参数是单值的情况
String value = entry.getValue()[0];
// 得到请求的参数对应的是第几个形参
int indexRequestParameterIndex = getIndexRequestParameterIndex(myHandler.getMethod(), key);
if (indexRequestParameterIndex != -1) {
params[indexRequestParameterIndex] = value;
}else {
// 说明并没有找到@RequestParam注解对应的参数,就会使用默认的机制进行配置
// 得到请求参数对应的是第几个没有@RequestParam注解修饰的形参
int parameterIndex = getParameterIndex(myHandler.getMethod(), key);
if (parameterIndex != -1) {
// 填充到实参数组
params[parameterIndex] = value;
}
}
}
(3)修改Java类 MonsterController 中的 findMonsterByName 方法,删除RequestParam注解
@RequestMapping(value = "/monster/find")
public void findMonsterByName(HttpServletRequest request,
HttpServletResponse response,
String name) {
(4)启动tomcat服务器,测试效果。在地址栏输入 http://localhost:8080/springmvc/monster/find?name=精 ,显示如下页面,测试成功 
2.7 阶段 7- 完成简单视图解析
功能说明:通过方法返回的 String, 转发或者重定向到指定页面
完成任务说明:
- 用户输入白骨精,可以登录成功, 否则失败
- 根据登录的结果, 可以重定向或者请求转发到 login_ok.jsp / login_error.jsp, 并显示妖怪名
(1)在web目录下,编写三个jsp页面:login.jsp,login_ok.jsp,login_error.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>登录页面</title>
</head>
<body>
<h1>登录页面</h1>
<form action="monster/login" method="post">
妖怪名: <input type="text" name="mName"><br/>
<input type="submit" value="登录">
</form>
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
<title>登录成功</title>
</head>
<body>
<h1>登录成功</h1>
欢迎你: ${requestScope.mName}
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
<title>登录失败</title>
</head>
<body>
<h1>登录失败</h1>
sorry, 登录失败 ${requestScope.mName}
</body>
</html>
(2)修改controller包中的MonsterController类,增加如下方法
//处理妖怪登录的方法,返回要请求转发/重定向的字符串
@RequestMapping("/monster/login")
public String login(HttpServletRequest request,
HttpServletResponse response,
String mName) {
System.out.println("--接收到mName---" + mName);
//将mName设置到request域
request.setAttribute("mName", mName);
boolean b = monsterServiceImpl.login(mName);
if (b) {//登录成功!
//return "forward:/login_ok.jsp";
//测试重定向
//return "redirect:/login_ok.jsp";
//测试默认的方式-forward
return "/login_ok.jsp";
} else {//登录失败
return "forward:/login_error.jsp";
}
}
(3)修改service包下的MonsterService接口,增加如下方法
//增加方法,处理登录
public boolean login(String name);
(4)修改service.impl包的MonsterServiceImpl类,增加如下方法
@Override
public boolean login(String name) {
//实际上会到DB验证->这里模拟
if ("白骨精".equals(name)) {
return true;
} else {
return false;
}
}
(5)在 类 DispatcherServlet.java 中的 executeDispatch 方法中 以下位置添加如下代码 可以解决web页面提交的数据中文乱码的问题
//处理提交的数据中文乱码
request.setCharacterEncoding("utf-8");
(6)在 类 DispatcherServlet.java 中添加方法 viewResolver 充当视图解析器
/**
* 该方法作为视图解析器来使用
* 原生SpringMVC是把这个方法单独写在视图解析类中,由前端控制器来调用
* 这里做了简化,直接写在前端控制器中
*/
private void viewResolver(Object result, HttpServletRequest request, HttpServletResponse response) {
String viewName = (String) result;
try {
if (viewName.contains(":")) {
//说明返回的String 结果为 forward:/login_ok.jsp 或者 redirect:/xxx/xx/xx.xx
String viewType = viewName.split(":")[0];//forward | redirect
String viewPage = viewName.split(":")[1];//要跳转的页面名
// 判断是 forward 还是 redirect
if ("forward".equals(viewType)) {
//进行请求转发
request.getRequestDispatcher(viewPage).forward(request, response);
} else if ("redirect".equals(viewType)) {
//进行重定向,重定向需要加上项目路径
response.sendRedirect(request.getContextPath() + viewPage);
}
} else {
//默认请求转发
request.getRequestDispatcher(viewName).forward(request, response);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
(7)在 类 DispatcherServlet.java 中的 executeDispatch 方法中 以下位置添加如下代码 ,调用视图解析器
// 通过反射调用该方法,通过实参参数数组传入参数
Object result = myHandler.getMethod().invoke(myHandler.getController(), params);
if (result instanceof String) {// 调用视图解析器
viewResolver(result, request, response);
}

(8)启动tomcat服务器,测试效果。在地址栏输入 http://localhost:8080/springmvc/login.jsp ,显示如下页面,在输入框中输入白骨精

点击登录后,跳转至如下页面,即测试成功
2.8 完成返回 JSON 格式数据-@ResponseBody
功能说明:通自定义@ResponseBody 返回 JSON 格式数据
(1)在 annotation 包下创建注解 @ResponseBody
package com.myspringmvc.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {
}
(2)修改controller包中的MonsterController类,增加如下方法
/**
* 方法上标注了 @ResponseBody 表示希望以json格式返回给客户端/浏览器
*/
@RequestMapping("/monster/list/json")
@ResponseBody
public List<Monster> listMonsterByJson() {
List<Monster> monsters = monsterServiceImpl.listMonster();
return monsters;
}
(3) 在pom.xml文件中引入 jackson 包
<!--使用jackson工具类可以进行json操作-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.0</version>
</dependency>
(4)在 类 DispatcherServlet.java 中的 executeDispatch 方法中 以下位置添加如下代码,返回json格式数据
else if (result instanceof ArrayList) {
// 如果是ArrayList,再判断是否有@ResponseBody注解
if(myHandler.getMethod().isAnnotationPresent(ResponseBody.class)){
// 有@ResponseBody注解,就返回json数据
// 把result 转成json格式数据,可以使用jackson包下的工具类
ObjectMapper objectMapper = new ObjectMapper();
// 返回的结果resultJson,就是json格式的字符串
String resultJson = objectMapper.writeValueAsString(result);
// 这里简单处理,直接返回
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(resultJson);
writer.flush();
writer.close();
}
}
(5)启动tomcat服务器,测试效果。在地址栏输入 http://localhost:8080/springmvc/monster/list/json ,显示如下页面,测试成功

至此,实现任务结束!附一张原生SpringMVC工作流程图

:手动实现 SpringMVC 底层机制。本文章包含所有源码,且均有详细注释(核心分发控制器+ Controller容器控制器+方法获取参数.....,&spm=1001.2101.3001.5002&articleId=139354257&d=1&t=3&u=2770c09db1cd43ffa927cce30711d1d6)
2763

被折叠的 条评论
为什么被折叠?



