從頭開始實現一個小型spring框架——控制器controller實現mvc請求攔截和響應

寫在前面

最近學習了一下spring的相關內容,所以也就想要照貓畫虎地記錄和實現一下spring的框架,通過閱讀這些也希望能夠消除對Spring框架的恐懼,其實細心閱讀框架也很容易理解。
mini-spring儘量實現spring的核心功能。文章寫得很倉促,可能不是很全面,在全部完成之後我們再來完善表達吧,見諒~

項目的源碼我放在了github上:源碼地址

我會在這裏整理文章的系列目錄:

  1. 從頭開始實現一個小型spring框架——手寫Spring之實現SpringBoot啓動
  2. 從頭開始實現一個小型spring框架——手寫Spring之集成Tomcat服務器
  3. 從頭開始實現一個小型spring框架——控制器controller的實現
  4. 從頭開始實現一個小型spring框架——實現Bean管理(IOC與DI)

一、容器內對請求的處理過程

我們在上一篇博客裏講了,容器內具體請求的處理流程對於容器是一個黑盒,根據具體的實現而有所不同。下面我們就對典型的容器內部的處理流程進行簡單的總結和敘述。

1.1 請求典型流程

先看這麼一張圖片:

熟悉Servlet開發流程的都知道,我們需要在web.xml中配置我們的Servlet路徑和請求路徑,或者使用註解進行配置,纔可以完成URL和Servlet之間的映射。

我們把URI到Servlet的映射配置到Xml裏,當一個http請求到達服務器時,服務器會獲取這個請求的URI,然後去Web.xml中查找,通過映射表找到對應的Servlet,並把這個請求轉發到對應的Servlet,Servlet處理完後,再把結響應回去。這就是整個業務的處理流程了。

1.2 存在的問題

  • 服務器調度的問題
    配置集中,大而且雜亂,維護成本高
  • 需要多次實現Servlet接口

1.3 Spring的改進

Spring對這個問題的改進,是引入的一個總管一樣的方式進行管理,也就是我們非常熟悉的DispatcherServlet,用來受理所有的業務。

其中的差異可以對比下面這張圖片

tomcat統一將請求發送給DispatcherServlet,再由DispatcherServlet將這些請求派發給具體的Mapping Handler,處理完畢後返回。其中Servlet接口僅實現了一次。並且通過註解的方式進行配置,更簡化了我們的實現流程,分散配置,業務邏輯也就變得清晰明確。

二、mvc實現

2.1 變化後的包結構

本次實現新增加了

  1. framework模塊
  • core包中增加ClassScanner實現包掃描

  • 增加handler包實現反射獲取類信息

  • mvc包增加註解

    • Controller註解標誌類
    • RequestMapping註解方法
    • RequestParam註解參數
  • 修改servlet包實現全局 / 路徑下的uri攔截

  1. test模塊
  • 增加controller實現測試請求

2.2 framework模塊實現DispatcherServlet和反射獲取類信息

core包下的ClassScanner類


package com.qcby.core;

import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @author kevinlyz
 * @ClassName ClassScanner
 * @Description 通過類加載器獲取目錄下的類列表
 * @Date 2019-06-08 17:26
 **/
public class ClassScanner {

    public static List<Class<?>> scannClasses(String packageName) throws IOException, ClassNotFoundException {
        List<Class<?>> classes = new ArrayList<>();
        String path = packageName.replace(".","/");
        //獲取類加載器
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Enumeration<URL> resources = classLoader.getResources(path);
        while (resources.hasMoreElements()){
            URL resource = resources.nextElement();
            //處理資源類型是jar包的情況
            if (resource.getProtocol().contains("jar")){
                JarURLConnection jarURLConnection =
                        (JarURLConnection) resource.openConnection();
                String jarFilePath = jarURLConnection.getJarFile().getName();
                classes.addAll(getClassFromJar(jarFilePath,path));
            }else{
                // TODO: 處理jar包以外的情況
            }
        }
        return classes;
    }

    /**
     * @Author kevinlyz
     * @Description 從jar包中獲取資源
     * @Date 17:37 2019-06-08
     * @Param
     * @return List<Class<?>>
     **/
    public static List<Class<?>> getClassFromJar(String jarFilePath,String path) throws IOException, ClassNotFoundException {
        List<Class<?>> classes = new ArrayList<>();
        //獲取jar實例
        JarFile jarFile = new JarFile(jarFilePath);
        Enumeration<JarEntry> jarEntrys = jarFile.entries();
        while (jarEntrys.hasMoreElements()){
            JarEntry jarEntry = jarEntrys.nextElement();
            //獲取類路徑名  如  com/qcby/test/Test.class
            String entryName = jarEntry.getName();
            //獲取的
            if (entryName.startsWith(path)&&entryName.endsWith(".class")){
                //路徑替換
                String classFullName = entryName.replace("/",".").substring(0,entryName.length()-6);
                //反射獲取類信息並添加至list
                classes.add(Class.forName(classFullName));
            }
        }
        return classes;
    }
}

scannClasses之後會在MiniApplication中被調用,用於獲取類列表。

MiniApplication類,添加獲取類列表和反射調用。

package com.qcby.starter;

import com.qcby.core.ClassScanner;
import com.qcby.web.handler.HandlerManagger;
import com.qcby.web.server.TomcatServer;

import java.util.List;

/**
 * @author kevinlyz
 * @ClassName MiniApplication
 * @Description 框架的入口類
 * @Date 2019-06-04 19:21
 **/
public class MiniApplication {
    public static void run(Class<?> cls,String[] args){
        System.out.println("Hello mini-spring application!");
        TomcatServer tomcatServer = new TomcatServer(args);
        try {
            tomcatServer.startServer();
            List<Class<?>> classList = ClassScanner.scannClasses(cls.getPackage().getName());
            classList.forEach(it->System.out.println(it.getName()));
            HandlerManagger.resolveMappingHandler(classList);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

HandlerManager類

package com.qcby.web.handler;

import com.qcby.web.mvc.Controller;
import com.qcby.web.mvc.RequestMapping;
import com.qcby.web.mvc.RequestParam;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;

/**
 * @author kevinlyz
 * @ClassName HandlerManagger
 * @Description 反射獲取類信息
 * @Date 2019-06-08 18:34
 **/
public class HandlerManagger {

    public static List<MappingHandler> mappingHandlerList = new ArrayList<>();

    public static void resolveMappingHandler(List<Class<?>>  classList){
        for (Class<?> cls : classList){
            if (cls.isAnnotationPresent(Controller.class)){
                presentHandlerFromController(cls);
            }
        }
    }

    private static void presentHandlerFromController(Class<?> cls) {
        //獲取方法
        Method[] methods= cls.getDeclaredMethods();
        for (Method method : methods){
           if(!method.isAnnotationPresent(RequestMapping.class))
               continue;
           //獲取uri路徑
           String uri = method.getDeclaredAnnotation(RequestMapping.class).value();
           List<String> paramList = new ArrayList<>();
           //獲取參數值
           for (Parameter parameter : method.getParameters()){
               if (parameter.isAnnotationPresent(RequestParam.class)){
                   paramList.add(parameter.getDeclaredAnnotation(RequestParam.class).value());
               }
           }
           String[] params= paramList.toArray(new String[paramList.size()]);
           MappingHandler mappingHandler = new MappingHandler(uri,method,cls,params);
           HandlerManagger.mappingHandlerList.add(mappingHandler);
        }

    }

}

MappingHandler

package com.qcby.web.handler;


import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author kevinlyz
 * @ClassName MappingHandler
 * @Description 包含uri,類的方法信息,類信息和參數
 * @Date 2019-06-08 18:32
 **/
public class MappingHandler {

    private String uri;
    private Method method;
    private Class<?> controller;
    private String[] args;

    public MappingHandler(String uri, Method method, Class<?> controller, String[] args) {
        this.uri = uri;
        this.method = method;
        this.controller = controller;
        this.args = args;
    }

    public boolean handle(ServletRequest req, ServletResponse res) throws IllegalAccessException, InstantiationException, InvocationTargetException, IOException {
        String reqUri = ((HttpServletRequest)req).getRequestURI();
        if (!this.uri.equals(reqUri))
            return false;

        //相等則調用Handler的resolveMappingHandler方法,實例化並返回
        Object[] parameters = new Object[args.length];
        for(int i=0;i<args.length;i++){
            parameters[i] = req.getParameter(args[i]);
        }

        Object ctl = controller.newInstance();
        Object response = method.invoke(ctl,parameters);
        res.getWriter().println(response.toString());
        return true;
    }
}

mvc包下

Controller自定義註解

package com.qcby.web.mvc;


import java.lang.annotation.*;


/**
 * @Author kevinlyz
 * @Description 控制器註解,添加在Controller上
 * @Date 17:10 2019-06-08
 **/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Controller {

}

RequestMapping自定義註解

package com.qcby.web.mvc;


import java.lang.annotation.*;

/**
 * @Author kevinlyz
 * @Description 映射註解,添加在方法上
 * @Date 17:11 2019-06-08

 **/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequestMapping {
    String value();
}

RequestParam自定義註解

package com.qcby.web.mvc;

import java.lang.annotation.*;

/**
 * @Author kevinlyz
 * @Description 參數註解,添加在方法參數上
 * @Date 17:10 2019-06-08
 **/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestParam {
    String value();
}

server包下的TomcatServer(修改攔截uri爲 /,並將請求轉發至DispatcherServlet)

package com.qcby.web.server;

import com.qcby.web.servlet.DispatcherServlet;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.Tomcat;

/**
 * @author kevinlyz
 * @ClassName TomcatServer
 * @Description 集成Tomcat服務器,將請求轉發至DispatcherServlet
 * @Date 2019-06-05 13:10
 **/
public class TomcatServer {
    private Tomcat tomcat;
    private String[] agrs;

    public TomcatServer(String[] agrs) {
        this.agrs = agrs;
    }

    public void startServer() throws LifecycleException {
        //實例化tomcat
        tomcat = new Tomcat();
        tomcat.setPort(9999);
        tomcat.start();
        //實例化context容器
        Context context = new StandardContext();
        context.setPath("");
        context.addLifecycleListener(new Tomcat.FixContextListener());
        DispatcherServlet servlet = new DispatcherServlet();
        Tomcat.addServlet(context,"dispatcherServlet",servlet).setAsyncSupported(true);
        //添加映射
        context.addServletMappingDecoded("/","dispatcherServlet");
        tomcat.getHost().addChild(context);

        //設置常駐線程防止tomcat中途退出
        Thread awaitThread = new Thread("tomcat_await_thread."){
            @Override
            public void run() {
                TomcatServer.this.tomcat.getServer().await();
            }
        };
        //設置爲非守護線程
        awaitThread.setDaemon(false);
        awaitThread.start();
    }
}

servlet包:

package com.qcby.web.servlet;

import com.qcby.web.handler.HandlerManagger;
import com.qcby.web.handler.MappingHandler;

import javax.servlet.*;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;

/**
 * @author kevinlyz
 * @ClassName TestServlet
 * @Description 處理請求,請求攔截和匹配,若不存在對應uri則直接返回
 * @Date 2019-06-05 13:28
 **/
public class DispatcherServlet implements Servlet {
    @Override
    public void init(ServletConfig config) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        for (MappingHandler mappingHandler : HandlerManagger.mappingHandlerList){
            try {
                if (mappingHandler.handle(req,res)){
                    return;
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

至此,framework核心的請求攔截就處理完成了

2.3 test模塊測試

我們在test模塊下新建一個UserController類用於測試我們的請求過程。

package com.qcby.controller;

import com.qcby.web.mvc.Controller;
import com.qcby.web.mvc.RequestMapping;
import com.qcby.web.mvc.RequestParam;

import java.lang.annotation.Retention;

/**
 * @author kevinlyz
 * @ClassName UserController
 * @Description 測試請求
 * @Date 2019-06-08 17:14
 **/
@Controller
public class UserController {

    @RequestMapping("/test")
    public String test(@RequestParam("name") String name,@RequestParam("desc") String desc){
        System.out.println("test訪問了!");
        return "hello controller!";
    }
}

同樣gradle build

java -jar test/build/libs/test-1.0-SNAPSHOT.jar 

打開我們的瀏覽器,輸入http://localhost:9999/test
看到輸出的hello controller!

深藏功與名!!!

三、小結

今天我們對SpringMvc的核心功能進行了實現,通過使用包掃描和反射機制獲取到註解的信息,並進行實例化,最終實現請求的攔截和相應。
(寫的不是很詳細,改日再補)
test的Application向framework模塊的MiniApplication傳遞類信息,MiniApplication調用ClassScanner進行包掃描,通過類加載器掃描包中的類路徑(其中對jar包的信息做了進一步處理),並返回classList類列表至MiniApplication;進而使用HandlerManagger獲取類信息,並通過HandlerManager進行組裝。
TomcatServer則更改其攔截的請求uri路徑爲/,以攔截所有通過tomcat的請求,並轉發至DispatcherServlet,DispatcherServlet的service方法進行uri匹配,匹配成功則調用Handler的resolveMappingHandler方法,實例化並返回響應信息。
然後我們又通過test模塊書寫了UserController進行測試,成功返回hello controller的字符串。至此我們的mvc的請求控制功能就完全實現了,仔細想想是不是很簡單呢~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章