【SpringMVC原理】自己手動開發一個簡易版SpringMVC框架

1 【骨架】項目框架搭建

使用Gradle進行依賴管理

  • 首先在Idea中選擇File—>New—>Project,在左側選擇Gradle—>Java,填寫一些項目信息,這裏名爲mini-spring,創建項目完成
  • 之後,在項目上右鍵New—>Module,在左側選擇Gradle—>Java,ArtifactId填爲framework,創建模塊完成
  • 重複上面的步驟,創建一個名爲test的module,作爲測試mini-spring功能的模塊
  • 由於不需要在主模塊下編寫代碼,刪掉主目錄下的src目錄

至此,項目搭建完成,目錄結構如下:在這裏插入圖片描述
要編寫自己的spring框架需要先了解真正的spring的主要結構,如圖:
在這裏插入圖片描述
因此我們的mini-spring結構如下:

  • 實現core模塊,包括Beans、Core、Context包;
  • 實現web模塊,集成Web和WebMVC
  • 添加starter,實現類springboot的啓動方式

在framework模塊中創建如下的package:
在這裏插入圖片描述
在test模塊中添加啓動類Application並添加main方法輸出hello world語句。
在test模塊中的build.gradle文件添加如下代碼:

jar {
    manifest {
        attributes "Main-Class": "cn.edu.neusoft.Application"
    }
    from {
        configurations.compile.collect() {
            it.isDirectory() ? it : zipTree(it)
        }
    }
}

在terminal中使用gradle buildjava -jar命令查看是否成功輸出hello world

爲了把framework模塊打包到test模塊中,需要在dependencies中添加compile(project(':framework')),這樣test模塊打出的jar包就會包含framework模塊的代碼了

接着,在framework的starter包中建立一個類MiniApplication,添加如下方法:

    public static void run(Class<?> clz, String[] args) {
        System.out.println("hello mini-spring");
    }

這樣做是模擬了springboot的啓動方法,在test的Application類的main方法中調用MiniApplication.run(Application.class, args);
重新執行gradle buildjava -jar命令查看是否成功輸出hello world和hello mini-spring

2 【載體】集成服務器之Web服務器&Servlet

在這裏插入圖片描述
由於服務器進程的編寫涉及到Java的Socket編程、HTTP請求的封裝和服務器IO等知識,較爲複雜,這裏我們的框架仿照springboot的方式內置一個服務器(Tomcat)。

集成Tomcat
① 首先要爲容器引入一個Tomcat包,這裏選擇目前比較流行的8.5.23版本。在framework模塊的build.gradle文件的dependencies中引入compile group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '8.5.23'
②在web包下新建一個server子包並創建TomcatServer類,添加如下代碼:

public class TomcatServer {
    //tomcat配置
    private Tomcat tomcat;
    private String[] args;

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

    public void startServer() throws LifecycleException {
        tomcat = new Tomcat();
        tomcat.setPort(6699);//設置監聽端口
        tomcat.start();
        //防止服務器中途退出
        Thread awaitThread = new Thread("tomcat_await_thread") {
            @Override
            public void run() {
                //聲明tomcat一直在等待
                TomcatServer.this.tomcat.getServer().await();
            }
        };
        //設置線程爲非守護線程
        awaitThread.setDaemon(false);
        awaitThread.start();
    }
}

③在starter包下的MiniApplication類中添加

TomcatServer tomcatServer = new TomcatServer(args);
        try {
            tomcatServer.startServer();
        } catch (LifecycleException e) {
            e.printStackTrace();
        }

gradle build clean後再執行java -jar test-1.0-SNAPSHOT.jar看到控制檯輸出tomcat信息代表程序編寫成功(此時瀏覽器訪問http://localhost:6699會404,是因爲我們還沒有編寫servlet處理請求)
⑤修改TomcatServer類的代碼

public class TomcatServer {
    //tomcat配置
    private Tomcat tomcat;
    private String[] args;

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

    public void startServer() throws LifecycleException {
        tomcat = new Tomcat();
        tomcat.setPort(6699);//設置監聽端口
        tomcat.start();

        //建立tomcat與servlet的聯繫
        Context context = new StandardContext();
        context.setPath("");
        context.addLifecycleListener(new Tomcat.FixContextListener());
        TestServlet servlet = new TestServlet();
        Tomcat.addServlet(context, "TestServlet", servlet).setAsyncSupported(true);
        context.addServletMappingDecoded("/test.json", "TestServlet");
        tomcat.getHost().addChild(context);

        //防止服務器中途退出
        Thread awaitThread = new Thread("tomcat_await_thread") {
            @Override
            public void run() {
                //聲明tomcat一直在等待
                TomcatServer.this.tomcat.getServer().await();
            }
        };
        //設置線程爲非守護線程
        awaitThread.setDaemon(false);
        awaitThread.start();
    }
}

重新打包運行項目並訪問http://localhost:6699/test.json,看到輸出test servlet!,服務器正常響應

3 【入口】控制器Controller的實現

①修改TestServlet類名爲DispatcherServlet(這裏注意記得修改代碼中其它引用了TestServlet的地方),修改context.addServletMappingDecoded("/test.json", "TestServlet");context.addServletMappingDecoded("/", "DispatcherServlet");
②在web包中添加mvc包,並添加@Controller、@RequestMapping、@RequestParam三個常用的註解,代碼如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Controller {
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequestMapping {
    String value();
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestParam {
    String value();
}

③在test模塊中創建controllers包,並創建一個SalaryController類用來測試我們實現的註解

@Controller
public class SalaryController {
    @RequestMapping("getSalary.do")
    public Integer getSalary(@RequestParam("name") String name, @RequestParam("experience") String experience) {
        return 1000;
    }
}

④在core包中添加一個ClassScanner類添加如下代碼:

public class ClassScanner {
    public static List<Class<?>> scanClasses(String packageName) throws IOException, ClassNotFoundException {
        List<Class<?>> classList = 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();
                classList.addAll(getClassesFromJar(jarFilePath, path));
            } else {
                //todo
            }
        }
        return classList;
    }

    private static List<Class<?>> getClassesFromJar(String jarFilePath, String path) throws IOException, ClassNotFoundException {
        List<Class<?>> classes = new ArrayList<>();
        JarFile jarFile = new JarFile(jarFilePath);
        Enumeration<JarEntry> jarEntries = jarFile.entries();
        while (jarEntries.hasMoreElements()) {
            JarEntry jarEntry = jarEntries.nextElement();
            String entryName = jarEntry.getName();
            if (entryName.startsWith(path) && entryName.endsWith(".class")) {
                String classFullName = entryName.replace("/", ".").substring(0, entryName.length() - 6);
                classes.add(Class.forName(classFullName));
            }
        }
        return classes;
    }
}

⑤在框架入口類MiniApplication中修改run()方法爲:

public static void run(Class<?> clz, String[] args) {
        System.out.println("hello mini-spring");
        TomcatServer tomcatServer = new TomcatServer(args);
        try {
            tomcatServer.startServer();
            List<Class<?>> classList = ClassScanner.scanClasses(clz.getPackage().getName());
            classList.forEach(it -> System.out.println(it.getName()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

⑥運行結果
在這裏插入圖片描述
⑦在web包中添加handler包,添加MappingHandler類,代碼如下:

public class MappingHandler {
    private String uri;
    private Method method;
    private Class<?> controller;
    private String[] args;

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

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

        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;
    }
}

⑧在handler包中添加HandlerManager類,代碼如下:

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

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

    private static void parseHandlerFromController(Class<?> cls) {
        Method[] methods = cls.getDeclaredMethods();
        for (Method method : methods) {
            if (!method.isAnnotationPresent(RequestMapping.class)) {
                continue;
            }
            String uri = method.getDeclaredAnnotation(RequestMapping.class).value();
            List<String> paramNameList = new ArrayList<>();
            for (Parameter parameter : method.getParameters()) {
                if (parameter.isAnnotationPresent(RequestParam.class)) {
                    paramNameList.add(parameter.getDeclaredAnnotation(RequestParam.class).value());
                }
            }
            String[] params = paramNameList.toArray(new String[paramNameList.size()]);
            MappingHandler mappingHandler = new MappingHandler(uri, method, cls, params);
            HandlerManager.mappingHandlerList.add(mappingHandler);
        }
    }
}

⑨在DispatcherServlet中的service方法中使用這些handler:

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

⑩別忘了在MiniApplication中調用HandlerManager初始化所有的MappingHandler,添加HandlerManager.resolveMappingHandler(classList);

最後打包運行並在瀏覽器中訪問localhost:6699/getSalary.do,輸出1000,成功。

4 【基石】Bean管理(IOC&DI)

①仿照sring添加兩個bean相關的註解,在beans包下編寫如下代碼:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface AutoWired {
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Bean {
}

②創建一個bean工廠,編寫如下代碼:

public class BeanFactory {
    private static Map<Class<?>, Object> classToBean = new ConcurrentHashMap<>();

    public static Object getBean(Class<?> cls) {
        return classToBean.get(cls);
    }

    public static void initBean(List<Class<?>> classList) throws Exception {
        List<Class<?>> toCreate = new ArrayList<>();
        while (toCreate.size() != 0) {
            int remainSize = toCreate.size();
            for (int i = 0; i < toCreate.size(); i++) {
                if (finishCreate(toCreate.get(i))) {
                    toCreate.remove(i);
                }
            }
            if (toCreate.size() == remainSize) {
                throw new Exception("cycle dependency");
            }
        }
    }

    private static boolean finishCreate(Class<?> cls) throws IllegalAccessException, InstantiationException {
        if (!cls.isAnnotationPresent(Bean.class) && !cls.isAnnotationPresent(Controller.class)) {
            return true;
        }

        Object bean = cls.newInstance();
        for (Field field : cls.getDeclaredFields()) {
            if (field.isAnnotationPresent(AutoWired.class)) {
                Class<?> filedType = field.getType();
                Object reliantBean = BeanFactory.getBean(filedType);
                if (reliantBean == null) {
                    return false;
                }
                field.setAccessible(true);
                field.set(bean, reliantBean);
            }
        }
        classToBean.put(cls, bean);
        return true;
    }
}

③在啓動類中使用bean工廠初始化類,修改代碼如下:

try {
            tomcatServer.startServer();
            List<Class<?>> classList = ClassScanner.scanClasses(clz.getPackage().getName());
            BeanFactory.initBean(classList);
            HandlerManager.resolveMappingHandler(classList);
            classList.forEach(it -> System.out.println(it.getName()));
        }

④修改MappingHandler中的Object ctl = controller.newInstance();Object ctl = BeanFactory.getBean(controller);
⑤在test模塊中添加service包,並添加一個測試類:

@Bean
public class SalaryService {
    public Integer calSalary(Integer experience) {
        return experience * 500;
    }
}

⑥修改SalaryController的代碼:

@Controller
public class SalaryController {
    @AutoWired
    private SalaryService salaryService;

    @RequestMapping("getSalary.do")
    public Integer getSalary(@RequestParam("name") String name, @RequestParam("experience") String experience) {
        System.out.println("執行了controller方法");
        return salaryService.calSalary(Integer.parseInt(experience));
    }
}

⑦編譯運行項目在瀏覽器訪問http://localhost:6699/getSalary.do?experience=3&name=aa返回對應的計算結果,成功

至此,我們的簡易版SpringMVC的編寫就完成了
我已經將完整代碼放在Github上了
https://github.com/Fieeeeee/MySpringMVC.git
謝謝

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