28

框架—记一次手写简易MVC框架的过程 附源代码

 4 years ago
source link: https://studygolang.com/articles/26004
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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 整体调用图

uYFbQfE.png!web

image

1.3 启动加载顺序

aEBBVvM.jpg!web

image

2.具体实现

2.1 项目整体工程目录

nERbmin.png!web

image

  • 创建项目就不说了,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
aQnAZfu.png!web

image

待改进的一些点

  • 异常处理,框架里面的异常我很多都是直接答应堆栈信息,并没有处理。
  • BeanFactory很简陋,因为是简易,所以真的很简易。不支持多例。大家可以试试加
  • 扩展性很差,小弟能力有限,希望大佬轻喷。
  • .......

写在最后

  • 项目的github: https://github.com/ilssio/ilss-mvc
  • 项目很简单,有很多地方还有不足,大家可以一起来改改。后面等我内功深厚了,我会再战它的。

关于我

  • 坐标杭州,普通本科在读,计算机科学与技术专业,20年毕业,目前处于实习阶段。
  • 主要做Java开发,会写点Golang、Shell。对微服务、大数据比较感兴趣,预备做这个方向。
  • 目前处于菜鸟阶段,各位大佬轻喷,小弟正在疯狂学习。
  • 欢迎大家和我交流鸭!!!

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK