框架—記一次手寫簡易MVC框架的過程 附源代碼

0.環境

Java : JDK 1.8
IDE  : IDEA 2019
構建工具 : Gradle
複製代碼

1.總體思路

1.1 一些點

  • 使用DispatcherServlet統一接收請求
  • 自定義@Controller、@RequestMapping、@RequestParam註解來實現對應不一樣URI的方法調用
  • 使用反射用HandlerMapping調用對應的方法
  • 使用tomcat-embed-core內嵌web容器Tomcat.
  • 自定義簡單的BeanFactory實現依賴注入DI,實現@Bean註解和@Controller註解的Bean管理

1.2 總體調用圖

1.3 啓動加載順序

2.具體實現

2.1 項目總體工程目錄

  • 建立項目就不說了,IDEA自行建立gradle項目就好。

2.2 具體實現

  1. 在web.server下建立TomcatServer類
  • 簡單來講就是實例化一個tomcat服務,並實例化一個DispatcherServlet加入到context中,設置支持異步,處理全部的請求
public class TomcatServer {
    private Tomcat tomcat;
    private String[] args;

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

    public void startServer() throws LifecycleException {
        // instantiated Tomcat
        tomcat = new Tomcat();
        tomcat.setPort(6699);
        tomcat.start();

        Context context = new StandardContext();
        context.setPath("");
        context.addLifecycleListener(new Tomcat.FixContextListener());

        // register Servlet
        DispatcherServlet dispatcherServlet = new DispatcherServlet();
        Tomcat.addServlet(context, "dispatcherServlet", dispatcherServlet).setAsyncSupported(true);
        context.addServletMappingDecoded("/", "dispatcherServlet");
        tomcat.getHost().addChild(context);

        Thread awaitThread = new Thread(() -> TomcatServer.this.tomcat.getServer().await(), "tomcat_await_thread");
        awaitThread.setDaemon(false);
        awaitThread.start();
    }
}
複製代碼
  1. 在web.servlet中新建DispatcherServlet實現Servlet接口.
  • 由於是作一個簡單的MVC,這裏我直接處理全部請求,不分GET和POST,能夠自行改進。
  • 處理全部請求 只須要在service方法中處理便可。
  • 簡單的思路是,用HandlerManager經過URI在Map對象中獲取到對應MappingHandler對象,而後調用handle方法。
@Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        try {
            MappingHandler mappingHandler = HandlerManager.getMappingHandlerByURI(((HttpServletRequest) req).getRequestURI());
            if (mappingHandler.handle(req, res)) {
                return;
            }
        } catch (IllegalAccessException | InstantiationException | InvocationTargetException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
複製代碼
  1. 在web.handler中分別新建MappingHandler和HandlerManager兩個類。
  • MappingHandler用來存儲URI調用信息,像URI、Method 和 調用參數 這些。以下
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;
    }
}
複製代碼
  • 而HandlerManager則是負責把對應的URI和處理的MappingHandler對應起來
  • 實現就是用本身定義的類掃描器把全部掃描到的類傳進來遍歷,找出帶有Controller註解的類
  • 而後針對每一個Controller中含有RequestMapping註解的方法信息構建MappingHandler對象進行註冊,放入Map中。
public class HandlerManager {
    public static Map<String, MappingHandler> handleMap = new HashMap<>();

    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.handleMap.put(uri, mappingHandler);
        }
    }

    public static MappingHandler getMappingHandlerByURI(String uri) throws ClassNotFoundException {
        MappingHandler handler = handleMap.get(uri);
        if (null == handler) {
            throw new ClassNotFoundException("MappingHandler was not exist!");
        } else {
            return handler;
        }
    }
}
複製代碼
  • 而後在MappingHandler中加入handle方法,對請求進行處理。
public boolean handle(ServletRequest req, ServletResponse res) throws IllegalAccessException, InstantiationException, InvocationTargetException, IOException {
        String requestUri = ((HttpServletRequest) req).getRequestURI();
        if (!uri.equals(requestUri)) {
            return false;
        }

        // read parameters.
        Object[] parameters = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            parameters[i] = req.getParameter(args[i]);
        }

        // instantiated Controller.
        Object ctl = BeanFactory.getBean(controller);

        // invoke method.
        Object response = method.invoke(ctl, parameters);
        res.getWriter().println(response.toString());
        return true;
    }
複製代碼
  • 幾個註解在web.mvc包中,定義以下:
@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();
}
複製代碼
  1. 建立類掃描器ClassScanner
  • 思路也簡單,用Java的類加載器,把類信息讀入,放到一個List中返回便可。
  • 我只處理了jar包類型。
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();

            if (resource.getProtocol().contains("jar")) {
                // get Class from jar package.
                JarURLConnection jarURLConnection = (JarURLConnection) resource.openConnection();
                String jarFilePath = jarURLConnection.getJarFile().getName();
                classList.addAll(getClassesFromJar(jarFilePath, path));
            } else {
                // todo other way.
            }
        }
        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;
    }
}
複製代碼
  1. 在beans包下建立BeanFactory類和@Autowired @Bean註解
  • 註解定義
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Autowired {
}

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Bean {
}
複製代碼
  • 相信細心的必定看到了我MappingHandler裏面的handle方法實際上是用BeanFactory調用的getBean。
  • BeanFactory的實現其實也很簡單。就是把類掃描器掃描到的類傳進來,吧帶有Controller和Bean註解的類放入map中,若是內部用Autowired註解就用內部依賴注入。只有單例模式。
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<>(classList);

        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<?> fieldType = field.getType();
                Object reliantBean = BeanFactory.getBean(fieldType);
                if (null == reliantBean) {
                    return false;
                }
                field.setAccessible(true);
                field.set(bean, reliantBean);
            }
        }
        classToBean.put(cls, bean);
        return true;
    }
}
複製代碼
  1. 啓動類
public class IlssApplication {
    public static void run(Class<?> cls, String[] args) {
        TomcatServer tomcatServer = new TomcatServer(args);
        try {
            tomcatServer.startServer();
            List<Class<?>> classList = ClassScanner.scanClasses(cls.getPackage().getName());
            BeanFactory.initBean(classList);
            HandlerManager.resolveMappingHandler(classList);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

複製代碼

2.3 關於測試模塊 test

  • 作測試 內部打包的時候須要在test項目中的build.gradle加入下面配置
jar {
    manifest {
        attributes "Main-Class": "io.ilss.framework.Application"
    }

    from {
        configurations.compile.collect {
            it.isDirectory() ? it : zipTree(it)
        }
    }
}
複製代碼
  1. 建立Service類
@Bean
public class NumberService {
    public Integer calNumber(Integer num) {
        return num;
    }
}
複製代碼
  1. 建立Controller類
@Controller
public class TestController {

    @Autowired
    private NumberService numberService;
    @RequestMapping("/getNumber")
    public String getSalary(@RequestParam("name") String name, @RequestParam("num") String num) {
        return numberService.calNumber(11111) + name + num ;
    }

}
複製代碼
  1. 建立Application啓動類
public class Application {
    public static void main(String[] args) {
        IlssApplication.run(Application.class, args);
    }
}
複製代碼
  1. 控制檯
gradle clean install 
java -jar mvc-test/build/libs/mvc-test-1.0-SNAPSHOT.jar
複製代碼
  1. 訪問網址
  • http://localhost:6699/getNumber?name=aaa&num=123

待改進的一些點

  • 異常處理,框架裏面的異常我不少都是直接答應堆棧信息,並無處理。
  • BeanFactory很簡陋,由於是簡易,因此真的很簡易。不支持多例。你們能夠試試加
  • 擴展性不好,小弟能力有限,但願大佬輕噴。
  • .......

寫在最後

  • 項目的github:github.com/ilssio/ilss…
  • 項目很簡單,有不少地方還有不足,你們能夠一塊兒來改改。後面等我內功深厚了,我會再戰它的。

關於我

  • 座標杭州,普通本科在讀,計算機科學與技術專業,20年畢業,目前處於實習階段。
  • 主要作Java開發,會寫點Golang、Shell。對微服務、大數據比較感興趣,預備作這個方向。
  • 目前處於菜鳥階段,各位大佬輕噴,小弟正在瘋狂學習。
  • 歡迎你們和我交流鴨!!!
相關文章
相關標籤/搜索