一、前言
这里解释一下为什么我要花好几天的时间手写一个SpringMVC的框架并且做一个总结呢?首先我是希望通过这种方式来了解SpringMVC的启动流程大概做了哪些工作,其次这是一个简易版的实现,还会有一些BUG和功能上的不足。我只实现了主线流程,因为看源码主要还是需要掌握其核心流程做了什么,所以我也希望通过这种方式来进行源码阅读后的记录和总结。工程我已经搭建好并测试过了,注释也写得比较详细,感兴趣的小伙伴可以关注私聊我给你们发工程地址~
二、实现思路
- 1、创建Maven工程
- 2、创建控制层、业务层代码
- 3、准备自定义注解和SpringMVC核心配置文件springmvc.xml
- 4、准备前端控制器DispatcherServlet,并在web.xml文件中声明自定义的前端控制器
- 5、创建Spring容器,通过DOM4J解析springmvc的XML文件
- 6、扫描springmvc中的控制器以及service类并实例化对象放入容器中【iocMap】
- 7、实现容器中对象的注入,比如将Service对象注入至Controller
- 8、建立请求映射地址与控制器以及方法之间的映射关系【MyHandler对象存储】
- 9、接收用户请求并进行分发操作【DispatcherServlet.doDispatcher()】
- 10、Controller方法调用以及不同类型的响应数据处理
三、代码结构
四、上干货~
1、创建Maven工程
在pom文件中引入一些必要的依赖:
<?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.zhbf.springmvc</groupId> <artifactId>springmvc</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <!--设置maven编译的属性--> <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> <dependencies> <!--导入servlet依赖--> <!--PS:SpringMvc底层还是依赖于servlet,所以需要导入这个包--> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope><!--编译和测试阶段使用--> </dependency> <!--apache.commons工具包--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.9</version> </dependency> <!--lombok包--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> <scope>provided</scope> </dependency> <!--jackson【json转换工具包】--> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.3</version> </dependency> <!--XML解析包--> <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency> </dependencies> <build> <plugins> <!--编译插件,用于把方法中的参数从arg[0],argp[1]转换成对应的参数名--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.7.0</version> <configuration> <source>1.8</source> <target>1.8</target> <compilerArgs> <arg>-parameters</arg> </compilerArgs> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> </project>
2、创建控制层、业务层代码
这里就不一一粘贴代码了,涉及到的代码如下:
3、准备自定义注解和SpringMVC核心配置文件springmvc.xml
springmvc.xml
<?xml version="1.0" encoding="UTF-8"?> <beans> <!-- 配置创建容器时要扫描的包--> <component-scan base-package="com.zhbf.business.controller,com.zhbf.business.service"></component-scan> </beans>
自定义注解
/** * 自定义注解【AutoWired】 * -- @Retention: 注解保留策略 * -- @Target: 注解作用域 * * @author 有梦想的肥宅 * @date 2021/08/24 */ @Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(value = RetentionPolicy.RUNTIME)//运行时保留,可以通过反射读取 public @interface AutoWired { } /** * 自定义注解【Controller】 * * @author 有梦想的肥宅 * @date 2021/08/24 */ @Target(ElementType.TYPE)//能在类、接口(包含注解类型)和枚举类型上使用 @Retention(RetentionPolicy.RUNTIME) public @interface Controller { String value() default ""; } /** * 自定义注解【RequestMapping】 * * @author 有梦想的肥宅 * @date 2021/08/24 */ @Target({ElementType.TYPE, ElementType.METHOD})//能在类、接口、方法和枚举类型上使用 @Retention(RetentionPolicy.RUNTIME) public @interface RequestMapping { String value() default ""; } /** * 自定义注解【ResponseBody】 * * @author 有梦想的肥宅 * @date 2021/08/24 */ @Target({ElementType.TYPE, ElementType.METHOD})//能在类、接口、方法和枚举类型上使用 @Retention(RetentionPolicy.RUNTIME) public @interface ResponseBody { } /** * 自定义注解【ResponseBody】 * * @author 有梦想的肥宅 * @date 2021/08/24 */ @Target(ElementType.TYPE)//能在类、接口(包含注解类型)和枚举类型上使用 @Retention(RetentionPolicy.RUNTIME) public @interface Service { String value(); }
4、准备前端控制器DispatcherServlet,并在web.xml文件中声明自定义的前端控制器
web.xml
<!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>有梦想的肥宅手写SpringMVC测试</display-name> <!--1、配置前端控制器--> <servlet> <servlet-name>DispatcherServlet</servlet-name> <servlet-class>com.zhbf.springmvc.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> <!--2、配置web应用程序启动时加载servlet (1)load-on-startup 元素标记容器是否应该在web应用程序启动的时候就加载这个servlet,(实例化并调用其init()方法)。 (2)它的值必须是一个整数,表示servlet被加载的先后顺序。 (3)如果该元素的值为负数或者没有设置,则容器会当Servlet被请求时再加载。 (4)如果值为正整数或者0时,表示容器在应用启动时就加载并初始化这个servlet,值越小,servlet的优先级越高,就越先被加载。值相同时,容器就会自己选择顺序来加载。 --> <load-on-startup>1</load-on-startup> </servlet> <!--3、将请求映射到对应的Servlet,“/”表示映射所有请求--> <servlet-mapping> <servlet-name>DispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
DispatcherServlet
/** * 自定义前端控制器 * * @author 有梦想的肥宅 * @date 2021/08/24 */ public class DispatcherServlet extends HttpServlet { //自定义的SpringMvc容器 private WebApplicationContext webApplicationContext; //创建集合:用于存放自定义映射关系对象,处理请求直接从该集合中进行匹配 List<MyHandler> handList = new ArrayList<>(); /** * 初始化方法【主线流程】 * * @throws ServletException */ @Override public void init() throws ServletException { //1、加载初始化参数【读取web.xml中配置的参数contextConfigLocation对应的值】 String contextConfigLocation = this.getServletConfig().getInitParameter("contextConfigLocation"); //2、创建Springmvc容器 webApplicationContext = new WebApplicationContext(contextConfigLocation); //3、进行初始化操作 try { webApplicationContext.init(); } catch (Exception e) { System.out.println("初始化自定义IOC容器异常:" + e.getMessage()); e.printStackTrace(); } //4、初始化请求映射关系 initHandlerAdapter(); } /** * 初始化请求映射关系 */ private void initHandlerAdapter() { //1、轮询IOC容器集合 for (Map.Entry<String, Object> entry : webApplicationContext.iocMap.entrySet()) { //2、获取bean的class类型 Class<?> clazz = entry.getValue().getClass(); //3、判断是否为Controller if (clazz.isAnnotationPresent(Controller.class)) { //3.1 获取Controller的RequestMapping String controllerMapping = ""; if (clazz.isAnnotationPresent(RequestMapping.class)) { controllerMapping = clazz.getAnnotation(RequestMapping.class).value(); } //4、获取bean中所有的方法,为这些方法建立映射关系 Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { //5、如果含有注解RequestMapping则建立映射关系 if (method.isAnnotationPresent(RequestMapping.class)) { RequestMapping requestMapping = method.getAnnotation(RequestMapping.class); String url = controllerMapping + requestMapping.value();//获取注解中的值 //6、建立RequestMapping地址与控制器方法的映射关系,保存到MyHandler对象中 MyHandler myHandler = new MyHandler(url, entry.getValue(), method); //7、映射关系存入集合中 handList.add(myHandler); } } } } } /** * Get请求处理 */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { //PS:doGet内部调用doPost主要是为了统一处理方便 this.doPost(request, response); } /** * Post请求处理 */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { //进行请求分发处理 this.doDispatcher(request, response); } /** * 进行请求分发处理 */ public void doDispatcher(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { //1、根据用户的请求地址找到对应的自定义映射关系对象【模拟HandlerMapping】 MyHandler myHandler = getHandler(req); //2、获取方法返回对象 Object result = null; if (myHandler == null) { resp.getWriter().print("<h1>404 NOT FOUND!</h1>"); } else { //2.1 获取目标方法【模拟HandlerAdapter】 try { result = myHandler.getMethod().invoke(myHandler.getController()); } catch (Exception e) { e.printStackTrace(); } //2.2 设置响应内容 if (result instanceof String) {//跳转JSP String viewName = (String) result; //判断返回的路径 forward:/test.jsp if (viewName.contains(":")) { String viewType = viewName.split(":")[0]; String viewPage = viewName.split(":")[1]; if (viewType.equals("forward")) {//转发 req.getRequestDispatcher(viewPage).forward(req, resp); } else {//重定向 resp.sendRedirect(viewPage); } } else {//默认转发 req.getRequestDispatcher(viewName).forward(req, resp); } } else {//返回JSON格式数据 Method method = myHandler.getMethod(); if (method.isAnnotationPresent(ResponseBody.class)) { //将返回值转换成 json格式数据 ObjectMapper objectMapper = new ObjectMapper(); String json = objectMapper.writeValueAsString(result); resp.setContentType("text/html;charset=utf-8"); PrintWriter writer = resp.getWriter(); writer.print(json); writer.flush(); writer.close(); } } } } /*** * 获取请求对应的handler * @param req * @return */ public MyHandler getHandler(HttpServletRequest req) { // 1、读取请求的URI String requestURI = req.getRequestURI(); //2、从容器的Handle取出URL和用户的请求地址进行匹配,找到满足条件的Handler for (MyHandler myHandler : handList) { if (myHandler.getUrl().equals(requestURI)) { return myHandler; } } return null; } }
5、创建Spring容器,通过DOM4J解析springmvc的XML文件
/** * 应用上下文【Spring的IOC容器】 * * @author 有梦想的肥宅 * @date 2021/08/24 */ public class WebApplicationContext { //定义容器配置的路径:classpath:springmvc.xml String contextConfigLocation; //定义List:用于存放bean的类路径【用于反射创建对象】 List<String> classNameList = new ArrayList<String>(); //定义IOC容器:key存放bean的名字,value存放bean实例 public Map<String, Object> iocMap = new ConcurrentHashMap<>(); /** * 无参构造方法 */ public WebApplicationContext() { } /** * 有参构造方法 * * @param contextConfigLocation */ public WebApplicationContext(String contextConfigLocation) { this.contextConfigLocation = contextConfigLocation; } /** * 初始化Spring容器 */ public void init() throws Exception { //1、解析springmvc.xml配置文件【com.zhbf.business.*】 String pack = XmlPaser.getbasePackage(contextConfigLocation.split(":")[1]); String[] packs = pack.split(","); //2、进行包扫描 for (String pa : packs) { excuteScanPackage(pa); } //3、实例化容器中bean executeInstance(); //4、bean自动注入 executeAutoWired(); } /** * 扫描包 * * @author 有梦想的肥宅 */ private void excuteScanPackage(String pack) { //1、把包路径转换为文件目录 com.zhbf.business ==> com/zhbf/business URL url = this.getClass().getClassLoader().getResource("/" + pack.replaceAll("\\.", "/")); String path = url.getFile(); //2、通过IO流读取文件并解析 File dir = new File(path); for (File f : dir.listFiles()) { if (f.isDirectory()) { //若当前为文件目录,则递归继续进行扫描 excuteScanPackage(pack + "." + f.getName()); } else { //若当前为文件,则获取全路径 UserController.class ==> com.zhbf.business.controller.UserController String className = pack + "." + f.getName().replaceAll(".class", ""); //3、存放bean的类路径【用于反射创建对象】 classNameList.add(className); } } } /** * 实例化容器中的bean * * @author 有梦想的肥宅 */ private void executeInstance() throws Exception { // 1、循环存放bean的类路径的集合【用于反射创建对象】 for (String className : classNameList) { //2、获取Class对象 Class<?> clazz = Class.forName(className); //3、根据注解判断是Controller还是Service if (clazz.isAnnotationPresent(Controller.class)) { //Controller String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase() + clazz.getSimpleName().substring(1); iocMap.put(beanName, clazz.newInstance()); } else if (clazz.isAnnotationPresent(Service.class)) { //Service【如果是Service则读取@Service注解中设置的BeanName】 Service serviceAn = clazz.getAnnotation(Service.class); String beanName = !StringUtils.isEmpty(serviceAn.value()) ? serviceAn.value() : clazz.getSimpleName().substring(0, 1).toLowerCase() + clazz.getSimpleName().substring(1); iocMap.put(beanName, clazz.newInstance()); } } } /** * 进行自动注入操作 * * @author 有梦想的肥宅 */ private void executeAutoWired() throws IllegalAccessException { //1、从容器中取出bean并判断bean中是否有属性上使用了@AutoWired注解,如果使用了,就需要进行自动注入操作 for (Map.Entry<String, Object> entry : iocMap.entrySet()) { //2、获取容器中的bean Object bean = entry.getValue(); //3、获取bean中的属性 Field[] fields = bean.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(AutoWired.class)) { //4、获取属性值 String beanName = field.getName(); field.setAccessible(true);//取消检查机制 //5、在bean中设置iocMap.get(beanName)获得的对象 field.set(bean, iocMap.get(beanName)); } } } } }
6、扫描springmvc中的控制器以及service类并实例化对象放入容器中【iocMap】
详见WebApplicationContext.excuteScanPackage(String pack)方法。
7、实现容器中对象的注入,比如将Service对象注入至Controller
详见WebApplicationContext.executeAutoWired()方法。
8、建立请求映射地址与控制器以及方法之间的映射关系【MyHandler对象存储】
详见DispatcherServlet.initHandlerAdapter()和DispatcherServlet.getHandler(HttpServletRequest req)方法。
9、接收用户请求并进行分发操作【DispatcherServlet.doDispatcher()】
详见DispatcherServlet.doDispatcher()方法。
10、Controller方法调用以及不同类型的响应数据处理
详见DispatcherServlet.doDispatcher()方法中2.2注释下的代码实现。
五、调用测试
PS:上面所放出的代码不是所有的代码,不过最主线核心的代码已经放在上面了,感兴趣的小伙伴可以自己尝试着补充未写在博客上的代码,或者联系我获取所有源码~共同探讨进步,共勉!