自己实现SpringMVC 底层机制[一]
搭建SpringMVC 底层机制开发环境
- 1创建Maven 项目
- 2对Myspringmvc 进行配置: 修改my-springmvc\pom.xml , 将1.7修改成1.8.
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>
- 3对my-springmvc 进行配置: 创建main 和test 相关的源码目录和资源目录和测试目录
- 4引入需要的基本的jar 包, 修改my-springmvc\pom.xml
<dependencies> <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>
自己实现SpringMVC 底层机制【核心分发控制器+ Controller 和Service 注入容器+ 对象自动装配+ 控制器方法获取参数+ 视图解析+ 返回JSON 格式数据】
实现任务阶段1- 开发myDispatcherServlet
编写myDispatcherServlet 充当原生的DispatcherServlet(即核心控制器)
- 1创建src\main\java\com\myspringmvc\servlet\myDispatcherServlet.java
public class myDispatcherServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doGet(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPost(req, resp); } }
- 2创建src\main\resources\myspringmvc.xml, 充当原生的applicationContext-mvc.xml 文件
(就是spring 的容器配置文件, 比如指定要扫描哪些包下的类), 先创建个空的文件
3修改src\main\webapp\WEB-INF\web.xml, 完成myDispatcherServlet 的配置
<!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> <display-name>Archetype Created Web Application</display-name> <!--配置myDispatcherServlet, 作为我们自己的前端控制器--> <servlet> <servlet-name>myDispatcherServlet</servlet-name> <servlet-class>com.myspringmvc.servlet.myDispatcherServlet</servlet-class> <!--给myDispatcherServlet配置参数,指定要操作的spring容器配置文件--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:myspringmvc.xml</param-value> </init-param> <!--myDispatcherServlet在tomcat启动时,就自动加载--> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>myDispatcherServlet</servlet-name> <!--因为myDispatcherServlet作为前端控制器,所以需要拦截所有请求,url-pattern配置 /--> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
配置Tomcat, 完成测试
修改my-springmvc\src\main\java\com\servlet\myDispatcherServlet.java
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()被调用"); } }
启动Tomcat,完成测试.
实现任务阶段2- 完成客户端/浏览器可以请求控制层
创建自己的Controller 和自定义注解
创建my-springmvc\src\main\java\com\controller\MonsterController.java
public class MonsterController { public void listMonsters(HttpServletRequest request, HttpServletResponse response) { response.setContentType("text/html;charset=utf-8"); try { PrintWriter printWriter = response.getWriter(); printWriter.write("<h1>妖怪列表</h1>"); } catch (IOException e) { e.printStackTrace(); } } }
创建自定义注解,my-springmvc\src\main\java\com\myspringmvc\annotation\Controller.java
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Controller { String value() default ""; }
创建自定义注解,my-springmvc\src\main\java\com\myspringmvc\annotation\RequestMapping.java
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RequestMapping { String value() default ""; }
配置myspringmvc.xml
在该文件指定,我们的springmvc 要扫描的包
<?xml version="1.0" encoding="UTF-8" ?> <beans> <component-scan base-package="com.controller"></component-scan> </beans>
编写XMLParser 工具类,可以解析myspringmvc.xml,得到要扫描的包
创建my-springmvc\src\main\java\com\myspringmvc\xml\XMLPaser.java
//XMLParser 用于解析spring配置文件 public class XMLParser { public static String getBasePackage(String xmlFile) { SAXReader saxReader = new SAXReader(); //通过得到类的加载路径-》获取到spring配置文件.[对应的资源流] InputStream inputStream = XMLParser.class.getClassLoader().getResourceAsStream(xmlFile); try { //得到xmlFile文档 Document document = saxReader.read(inputStream); Element rootElement = document.getRootElement(); Element componentScanElement = rootElement.element("component-scan"); Attribute attribute = componentScanElement.attribute("base-package"); String basePackage = attribute.getText(); return basePackage; } catch (Exception e) { e.printStackTrace(); } return ""; } }
创建my-springmvc\src\test\java\com\test\mySpringMvcTest.java 完成测试
public class mySpringMvcTest { @Test public void readXML() { String basePackage = XMLPaser.getbasePackage("myspringmvc.xml"); System.out.println("basePackage= " + basePackage); } }
开发myWebApplicationContext,充当Spring 容器-得到扫描类的全路径列表
即把指定的目录包括子目录下的java 类的全路径扫描到集合中,比如ArrayList。
扫描后的classFullPathList= [com.controller.MonsterController, com.service.impl.MonsterServiceImpl, com.service.MonsterService]
创建my-springmvc\src\main\java\com\myspringmvc\context\myWebApplicationContext.java
public class myWebApplicationContext { private ArrayList<String> classFullPathList = new ArrayList<>(); // 初始化自定义的spring 容器, 目标就是把要@Controller 等等初始化到容器中 public void init() { //这里是写的固定的spring容器配置文件=>做活 //String basePackage = XMLParser.getBasePackage("myspringmvc.xml"); String basePackage = XMLParser.getBasePackage(configLocation.split(":")[1]); //这时basePackage => com.controller,com.service String[] basePackages = basePackage.split(","); //遍历basePackages, 进行扫描 if (basePackages.length > 0) { for (String pack : basePackages) { scanPackage(pack);//扫描 } } //测试 System.out.println("扫描后的= classFullPathList=" + classFullPathList); } //创建方法,完成对包的扫描 //@param pack 表示要扫描的包,比如"com.controller" public void scanPackage(String pack) { //得到包所在的工作路径[绝对路径] //下面这句话的含义是 通过类的加载器,得到你指定的包对应的 工作路径[绝对路径] //比如 "com.controller" => url 是 D:\my-springmvc\target\my-springmvc\WEB-INF\classes\com\controller //细节说明: 1. 不要直接使用Junit测试, 否则 url null // 2. 启动tomcat来测试 URL url = this.getClass().getClassLoader() .getResource("/" + pack.replaceAll("\\.", "/")); //System.out.println("url=" + url); //根据得到的路径, 对其进行扫描,把类的全路径,保存到classFullPathList String path = url.getFile(); System.out.println("path= " + path); //在io中,把目录,视为一个文件 File dir = new File(path); //遍历dir[文件/子目录] for (File f : dir.listFiles()) { if (f.isDirectory()) {//如果是一个目录,需要递归扫描 scanPackage(pack + "." + f.getName()); } else { //说明:这时,你扫描到的文件,可能是.class, 也可能是其它文件 //就算是.class, 也存在是不是需要注入到容器 //目前先把文件的全路径都保存到集合,后面在注入对象到容器时,再处理 String classFullPath = pack + "." + f.getName().replaceAll(".class", ""); classFullPathList.add(classFullPath); } } } }
修改com\myspringmvc\servlet\MyWebApplicationContext.java , 增加
@Override public void init(ServletConfig config) throws ServletException { MyWebApplicationContext myWebApplicationContext = new myWebApplicationContext(); myWebApplicationContext.init(); }
启动Tomcat 完成测试,看看扫描是否成功. 需要使用Tomcat 启动方式完成测试,直接用Junit 测试URL 是null
完善myWebApplicationContext,充当Spring 容器-实例化对象到容器中。
将扫描到的类, 在满足条件的情况下(即有相应的注解@Controller @Service…), 反射注入到ioc 容器。
扫描后的ioc={monsterController=com.controller.MonsterController@690aac1e}。
修改com\myspringmvc\context\MyWebApplicationContext.java
public class MyWebApplicationContext { private ArrayList<String> classFullPathList = new ArrayList<>(); //ioc 用于存放反射后的bean 对象. private ConcurrentHashMap<String, Object> ioc = new ConcurrentHashMap<>(); // 初始化自定义的spring 容器, 目标就是把要@Controller 等等初始化到容器中 public void init() { //这里是写的固定的spring容器配置文件.?=>做活 //String basePackage = XMLParser.getBasePackage("Myspringmvc.xml"); String basePackage = XMLParser.getBasePackage(configLocation.split(":")[1]); //这时basePackage => com.controller,com.service String[] basePackages = basePackage.split(","); //遍历basePackages, 进行扫描 if (basePackages.length > 0) { for (String pack : basePackages) { scanPackage(pack);//扫描 } } System.out.println("扫描后的= classFullPathList=" + classFullPathList); //将扫描到的类, 反射到ico容器 executeInstance(); System.out.println("扫描后的 ioc容器= " + ioc); //完成注入的bean对象,的属性的装配 executeAutoWired(); System.out.println("装配后 ioc容器= " + ioc); } //创建方法,完成对包的扫描 /** * @param pack 表示要扫描的包,比如"com.controller" */ public void scanPackage(String pack) { //得到包所在的工作路径[绝对路径] //下面这句话的含义是 通过类的加载器,得到你指定的包对应的 工作路径[绝对路径] //比如 "com.controller" => url 是 D:\My-springmvc\target\My-springmvc\WEB-INF\classes\com\controller //细节说明: 1. 不要直接使用Junit测试, 否则 url null // 2. 启动tomcat来吃测试 URL url = this.getClass().getClassLoader() .getResource("/" + pack.replaceAll("\\.", "/")); //System.out.println("url=" + url); //根据得到的路径, 对其进行扫描,把类的全路径,保存到classFullPathList String path = url.getFile(); System.out.println("path= " + path); //在io中,把目录,视为一个文件 File dir = new File(path); //遍历dir[文件/子目录] for (File f : dir.listFiles()) { if (f.isDirectory()) {//如果是一个目录,需要递归扫描 scanPackage(pack + "." + f.getName()); } else { //说明:这时,你扫描到的文件,可能是.class, 也可能是其它文件 //就算是.class, 也存在是不是需要注入到容器 //目前先把文件的全路径都保存到集合,后面在注入对象到容器时,再处理 String classFullPath = pack + "." + f.getName().replaceAll(".class", ""); classFullPathList.add(classFullPath); } } } }
完成测试(提示: 启动tomcat 来加载MyDispatcherServlet 的方式测试)
完成请求URL 和控制器方法的映射关系
示意图分析
-将配置的@RequestMapping 的url 和对应的控制器-方法映射关系保存到集合中
如图
handlerList= [MyHandler{url='/monster/list', controller=com.controller.MonsterController@714dab1, method=public void com.controller.MonsterController.listMonsters(javax.servlet.http.HttpServletReques t,javax.servlet.http.HttpServletResponse)}]
创建src\main\java\com\myspringmvc\handler\MyHandler.java
//MyHandler 对象记录请求的 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{" + "url='" + url + '\'' + ", controller=" + controller + ", method=" + method + '}'; } }
修改src\main\java\com\myspringmvc\servlet\MyDispatcherServlet.java
public class MyDispatcherServlet extends HttpServlet { //存放url 和控制器-方法的映射关系 private ArrayList<MyHandler> handlerList = new ArrayList<>(); private MyWebApplicationContext myWebApplicationContext; @Override public void init(ServletConfig config) throws ServletException { myWebApplicationContext = new MyWebApplicationContext(); myWebApplicationContext.init(); //调用initHandlerMapping() 搞定完成控制器层url---> Controller ---> 方法的映射关系 initHandlerMapping(); //测试(启动Tomcat 装载MyDispatcherServlet 对象方式来测试) System.out.println("handlerList= " + handlerList); } @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()被调用"); } /** * 1. 完成控制器层url---> Controller ---> 方法的映射关系(该关系封装到 MyHandler 对象) * 2. 并放入到handlerList 集合中 * 3. 后面我们可以通过handlerList 结合找到某个url 请求对应的控制器的方法(!!!) */ private void initHandlerMapping() { if(myWebApplicationContext.ioc.isEmpty()) { throw new RuntimeException("spring ioc 容器为空"); } for(Map.Entry<String,Object> entry: myWebApplicationContext.ioc.entrySet()) { Class<?> clazz = entry.getValue().getClass(); if(clazz.isAnnotationPresent(Controller.class)) { Method[] declaredMethods = clazz.getDeclaredMethods(); for (Method declaredMethod : declaredMethods) { if(declaredMethod.isAnnotationPresent(RequestMapping.class)) { RequestMapping requestMappingAnnotation = declaredMethod.getAnnotation(RequestMapping.class); String url = requestMappingAnnotation.value(); handlerList.add(new MyHandler(url,entry.getValue(),declaredMethod)); } } } } } }
完成测试(启动Tomcat , 加载MyDispatcherServlet 方式), 注意看后台输出。
完成MyDispatcherServlet 分发请求到对应控制器方法
当用户发出请求,根据用户请求url 找到对应的控制器-方法, 并反射调用。
如果用户请求的路径不存在,返回404。
修改com\myspringmvc\servlet\MyDispatcherServlet.java
/** * 1. MyDispatcherServlet充当原生DispatcherServlet * 2. 本质是一个Servlet, 继承HttpServlet */ public class MyDispatcherServlet extends HttpServlet { //定义属性 handlerList , 保存MyHandler[url和控制器方法的映射] private List<MyHandler> handlerList = new ArrayList<>(); //定义属性myWebApplicationContext,自己的spring容器 MyWebApplicationContextmyWebApplicationContext = null; @Override public void init(ServletConfig servletConfig) throws ServletException { //获取到web.xml中的 contextConfigLocation /* <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:myspringmvc.xml</param-value> </init-param> */ String configLocation = servletConfig.getInitParameter("contextConfigLocation"); //创建自己的spring容器 myWebApplicationContext = new MyWebApplicationContext(configLocation); myWebApplicationContext.init(); //调用 initHandlerMapping , 完成url和控制器方法的映射 initHandlerMapping(); //输出handlerList System.out.println("handlerList初始化的结果= " + handlerList); } @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 { //System.out.println("--MyDispatcherServlet--doPost---"); //调用方法,完成分发请求 executeDispatch(req, resp); } //编写方法,完成url 和 控制器方法的映射 private void initHandlerMapping() { if (myWebApplicationContext.ioc.isEmpty()) { //判断当前的ioc容器是否为null return; } //遍历ioc容器的bean对象,然后进行url映射处理 //java基础 map的遍历 for (Map.Entry<String, Object> entry :myWebApplicationContext.ioc.entrySet()) { //先取出注入的Object的clazz对象 Class<?> clazz = entry.getValue().getClass(); //如果注入的Bean是Controller if (clazz.isAnnotationPresent(Controller.class)) { //取出它的所有方法 Method[] declaredMethods = clazz.getDeclaredMethods(); //遍历方法 for (Method declaredMethod : declaredMethods) { //判断该方法是否有@RequestMapping if (declaredMethod.isAnnotationPresent(RequestMapping.class)) { //取出@RequestMapping值->就是映射路径 RequestMapping requestMappingAnnotation = declaredMethod.getAnnotation(RequestMapping.class); //这里可以把工程路径+url //getServletContext().getContextPath() // /springmvc/monster/list String url = requestMappingAnnotation.value(); //创建MyHandler对象->就是一个映射关系 MyHandlermyHandler = new MyHandler(url, entry.getValue(), declaredMethod); //放入到handlerList handlerList.add(myHandler); } } } } } }
完成测试(启动Tomcat)。
注意
因为getRequestURL返回的是主机后的所有内容,为了和handlermapping存放的URL一致,需要把tomcat配置的application context修改成 /,
否则匹配不上,因为按照原来的得到的是my_springmvc/monster/list.
增加方法和控制器
创建my-springmvc\src\main\java\com\controller\OrderController.java
@Controller public class OrderController { @RequestMapping(value = "/order/list") public void listOrder(HttpServletRequest request, HttpServletResponse response) { //设置编码和返回类型 response.setContentType("text/html;charset=utf-8"); //获取writer返回信息 try { PrintWriter printWriter = response.getWriter(); printWriter.write("<h1>订单列表信息</h1>"); } catch (IOException e) { e.printStackTrace(); } } @RequestMapping(value = "/order/add") public void addOrder(HttpServletRequest request, HttpServletResponse response) { //设置编码和返回类型 response.setContentType("text/html;charset=utf-8"); //获取writer返回信息 try { PrintWriter printWriter = response.getWriter(); printWriter.write("<h1>添加订单...</h1>"); } catch (IOException e) { e.printStackTrace(); } } }