一、创建Maven工程

image.png
创建出来的工程结构如下

java以及resources文件夹为后面新增

image.png
配置tomcat
image.png
image.png
启动测试,如果能看到如下界面,说明配置成功
image.png

二、创建对应的目录结构

其中,cn.spectrumrpc.springmvc为手写的springmvc代码所在,cn.spectrumrpc.test为测试手写mvc的测试代码
annotation包下存放自定义注解,包括@Controller,@Service,@RequestMapping等
context包下存放自定义的一个简单的Spring容器,用来存储@Controller,@Service这些Bean
handler包下存放自定义的Handler,用来保存请求url与Controller的映射关系
servlet包下存放DispatcherServlet,类比于SpringMVC中的中央控制器
controller包下存放测试的Controller类
service包下存放测试的Service类
image.png

三、创建自定义注解/mvc配置文件/controller等

3.1 自定义注解

由于自定义注解相对较多,且没啥变化,这里不贴代码,详见最后的工程代码

3.2 mvc配置文件

配置扫描Controller&Service包

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans>
  3. <component-scan base-package="com.spectrumrpc.test.controller,com.spectrumrpc.test.service"/>
  4. </beans>

3.3 Controller

  1. @Controller
  2. public class UserController {
  3. @Autowired("userService")
  4. private UserService userService;
  5. @RequestMapping("/hello")
  6. public String hello() {
  7. return userService.hello();
  8. }
  9. }

3.4 Service

  1. public interface UserService {
  2. String hello();
  3. }
  4. @Service(value = "userService")
  5. public class UserServiceImpl implements UserService {
  6. @Override
  7. public String hello() {
  8. return "hello,my-mvc";
  9. }
  10. }

四、创建DispatcherServlet,将其注册到web.xml中

web.xml

  1. <!DOCTYPE web-app PUBLIC
  2. "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
  3. "http://java.sun.com/dtd/web-app_2_3.dtd" >
  4. <web-app>
  5. <display-name>Archetype Created Web Application</display-name>
  6. <servlet>
  7. <servlet-name>DispatcherServlet</servlet-name>
  8. <servlet-class>cn.spectrumrpc.springmvc.servlet.DispatcherServlet</servlet-class>
  9. <init-param>
  10. <param-name>contextConfigLocation</param-name>
  11. <param-value>classpath:springmvc.xml</param-value>
  12. </init-param>
  13. <load-on-startup>1</load-on-startup>
  14. </servlet>
  15. <servlet-mapping>
  16. <servlet-name>DispatcherServlet</servlet-name>
  17. <url-pattern>/</url-pattern>
  18. </servlet-mapping>
  19. </web-app>

五、创建Web容器,保存springmvc的配置文件路径

  1. public class WebApplicationContext {
  2. private String configLocation;
  3. public WebApplicationContext() {
  4. }
  5. public WebApplicationContext(String configLocation) {
  6. this.configLocation = configLocation;
  7. }
  8. }

六、刷新Web容器,将Controller&Service扫描进入到容器中

首先,需要读取springmvc的配置文件,此处通过Dom4J,读取XML配置,获取component-scan对应的节点中的base-package属性(即,这个方法返回的是[com.spectrumrpc.test.controller,com.spectrumrpc.test.service])

XML解析工具

  1. public class XmlParser {
  2. public static String getBasePackage(String xml){
  3. try {
  4. SAXReader saxReader=new SAXReader();
  5. InputStream inputStream = XmlParser.class.getClassLoader().getResourceAsStream(xml);
  6. //XML文档对象
  7. Document document = saxReader.read(inputStream);
  8. Element rootElement = document.getRootElement();
  9. Element componentScan = rootElement.element("component-scan");
  10. Attribute attribute = componentScan.attribute("base-package");
  11. String basePackage = attribute.getText();
  12. return basePackage;
  13. } catch (DocumentException e) {
  14. e.printStackTrace();
  15. } finally {
  16. }
  17. return "";
  18. }
  19. }

将读取到的包路径,进行扫描,保存到容器中

其中,包含如下几步

  1. 循环所有的包路径,将所在包以及子包下的类,通过反射进行初始化,保存到集合中
  2. 进行Controller类的属性赋值,将Controller中包含@Autowired的属性,从ioc容器中取出赋值。
  3. 这几部操作完成之后,此时的beans集合中,即包含了所有的Controller以及Service。 ```java package cn.spectrumrpc.springmvc.context;

import cn.spectrumrpc.springmvc.annotation.Autowired; import cn.spectrumrpc.springmvc.annotation.Controller; import cn.spectrumrpc.springmvc.annotation.Service; import cn.spectrumrpc.springmvc.xml.XmlParser;

import java.io.File; import java.lang.reflect.Field; import java.net.URL; import java.util.HashMap; import java.util.Map; public class WebApplicationContext {

  1. private String configLocation;
  2. private Map<String, Object> beans = new HashMap<>();
  3. public WebApplicationContext() {
  4. }
  5. public Map<String, Object> getBeans() {
  6. return beans;
  7. }
  8. public void setBeans(Map<String, Object> beans) {
  9. this.beans = beans;
  10. }
  11. public WebApplicationContext(String configLocation) {
  12. this.configLocation = configLocation;
  13. }
  14. public void onRefresh() {
  15. //
  16. String basePackage = XmlParser.getBasePackage(configLocation.split(":")[1]);
  17. String[] packages = basePackage.split(",");
  18. for (String pack : packages) {
  19. // 扫描包下的类,并通过反射进行初始化
  20. scanAndInit(pack);
  21. }
  22. // 将Controller中的Service属性,从ioc中取出,设置进去
  23. propertiesSet();
  24. }
  25. private void propertiesSet() {
  26. try {
  27. for (String beanName : beans.keySet()) {
  28. Object o = beans.get(beanName);
  29. Field[] declaredFields = o.getClass().getDeclaredFields();
  30. for (Field declaredField : declaredFields) {
  31. if (declaredField.isAnnotationPresent(Autowired.class)) {
  32. Autowired autowired = declaredField.getAnnotation(Autowired.class);
  33. String value = autowired.value();
  34. declaredField.setAccessible(true);
  35. declaredField.set(o, beans.get(value));
  36. }
  37. }
  38. }
  39. } catch (IllegalAccessException e) {
  40. e.printStackTrace();
  41. }
  42. }
  43. private void scanAndInit(String pack) {
  44. System.out.println("pack = " + pack);
  45. URL url = this.getClass().getClassLoader().getResource("/" + pack.replaceAll("\\.", "/"));
  46. String path = url.getFile();
  47. File dir = new File(path);
  48. for (File f : dir.listFiles()) {
  49. if (f.isDirectory()) {
  50. //当前是一个文件目录,递归进行初始化
  51. scanAndInit(pack + "." + f.getName());
  52. } else {
  53. //文件目录下文件 获取全路径
  54. String className = pack + "." + f.getName().replaceAll(".class", "");
  55. System.out.println("className = " + className);
  56. try {
  57. Class<?> clazz = Class.forName(className);
  58. // Controller的名字,默认采用,类名首字母小写,放入容器。Service采用注解配置的value
  59. if (clazz.isAnnotationPresent(Controller.class)) {
  60. //控制层 bean
  61. String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase() + clazz.getSimpleName().substring(1);
  62. beans.put(beanName, clazz.newInstance());
  63. } else if (clazz.isAnnotationPresent(Service.class)) {
  64. //Service层 bean
  65. Service serviceAn = clazz.getAnnotation(Service.class);
  66. String beanName = serviceAn.value();
  67. beans.put(beanName, clazz.newInstance());
  68. }
  69. } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
  70. e.printStackTrace();
  71. }
  72. }
  73. }
  74. }

}

  1. <a name="8fOY4"></a>
  2. # 七、在DispatcherServlet中进行容器的初始化,以及处理器映射器的初始化
  3. 在Servlet初始化的时候,进行ioc容器的创建,以及刷新,将Controller以及Service保存到容器中然后。
  4. ```java
  5. @Override
  6. public void init() throws ServletException {
  7. // 1. 获取 web.xml中配置的init-param
  8. String contextConfigLocation = this.getServletConfig().getInitParameter("contextConfigLocation");
  9. // 2. 让ioc容器去加载这个配置文件
  10. this.webApplicationContext = new WebApplicationContext(contextConfigLocation);
  11. // 3. 刷新容器
  12. webApplicationContext.onRefresh();
  13. // 4. 保存url与Controller,Method的映射关系
  14. initHandlerMappings();
  15. }

对应的组件保存到容器中之后,将进行处理器关系的映射。

  1. private void initHandlerMappings() {
  2. // 从ioc容器中取出所有的组件
  3. Map<String, Object> beans = webApplicationContext.getBeans();
  4. for (String key : beans.keySet()) {
  5. Object bean = beans.get(key);
  6. Method[] declaredMethods = bean.getClass().getDeclaredMethods();
  7. // 判断这些组件是否包含了@RequestMapping注解,如果包含了,说明对应请求
  8. for (Method declaredMethod : declaredMethods) {
  9. if (declaredMethod.isAnnotationPresent(RequestMapping.class)) {
  10. // 然后通过Handler保存这些映射关系,让如集合中
  11. RequestMapping requestMapping = declaredMethod.getAnnotation(RequestMapping.class);
  12. String value = requestMapping.value();
  13. MyHandler myHandler = new MyHandler(value, bean, declaredMethod);
  14. handlers.add(myHandler);
  15. }
  16. }
  17. }
  18. }

当接收到请求时,通过请求路径与Handler中的路径进行比较,找出对应的Handler,如果找不到,则返回404,如果查找到了,则通过反射,调用对应的Method,完成一次请求。最后,通过函数的返回值,以及ResponseBody注解,来判断这个是一个跳转视图的请求,还是一次json前后端分离的请求,通过不同的格式,相应不同的数据。

  1. private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws IOException {
  2. MyHandler handler = getHandler(req);
  3. if (handler == null) {
  4. resp.getWriter().print("404 NOT FOUNT");
  5. } else {
  6. try {
  7. Method method = handler.getMethod();
  8. Object invoke = method.invoke(handler.getController());
  9. if (method.isAnnotationPresent(ResponseBody.class)) {
  10. if (invoke instanceof String) {
  11. resp.getWriter().write(invoke.toString());
  12. } else {
  13. resp.setContentType("application/json;charset=utf-8");
  14. resp.getWriter().write(JSONObject.toJSONString(invoke));
  15. }
  16. } else {
  17. if (invoke.toString().startsWith("redirect:")) {
  18. resp.sendRedirect(invoke.toString() + ".jsp");
  19. } else {
  20. req.getRequestDispatcher(invoke.toString().replaceFirst("forward:", "") + ".jsp").forward(req, resp);
  21. }
  22. }
  23. } catch (IllegalAccessException | IOException | InvocationTargetException | ServletException e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }
  28. private MyHandler getHandler(HttpServletRequest request) {
  29. String requestURI = request.getRequestURI();
  30. for (MyHandler handler : handlers) {
  31. if (handler.getUrl().equals(requestURI)) {
  32. return handler;
  33. }
  34. }
  35. return null;
  36. }

当然,此mvc源码,并未处理controller中的参数问题,
那么,接下来,我们就看看,参数如何进行处理
首先,都知道,通过method.getParameters(),可以获取到这个方法中的所有参数,在通过getName可以获取到参数的名称,但是,如果在默认情况下,获取到的是,arg0,arg1这种东西,根本没用。
在这种情况下,需要开启idea的一个功能,让其可以获取到真实的参数名称,详细步骤如下
在Settings下,添加 -parameters选项,即可获取到真实的参数名
image.png
然后,通过req.getParameter(name),根据真实的参数名称,从request中获取到请求参数,再通过反射调用即可。

  1. for (int i = 0; i < parameters.length; i++) {
  2. String name = parameters[i].getName();
  3. System.out.println("name = " + name);
  4. String param = req.getParameter(name);
  5. params[i] = param;
  6. }
  7. Object invoke = method.invoke(handler.getController(), params);

最后源码

my_spring_mvc.rar