zoukankan      html  css  js  c++  java
  • 自己动手实现mvc框架

      用过springmvc的可能都知道,要集成springmvc需要在web.xml中加入一个跟随web容器启动的DispatcherServlet,然后由该servlet初始化一些东西,然后所有的web请求都被这个servlet接管。所以自己写mvc的关键就是弄懂这个servlet干了啥。先分析一下springmvc的功能,首先我们写一个接口,就是写一个Controller,然后里面写一个方法,在类或者方法里面使用@RequestMapping直接修饰,当该注解对应的path被请求时,会按照指定格式传入参数并调用该方法,然后按照指定格式将调用的结果写出到向浏览器的输出流中(@ResponseBody),或者转发到jsp,再去由jsp转换生成的servlet去将结果写出到输出流(默认的请求转发),或者重定向到指定的jsp(return "redirect:/test.jsp"等。上面仅仅使用jsp举例子,不代表springmvc只支持使用jsp渲染。但是我们自己写的mvc只是为了演示整个流程和若干细节,并不能全面重写springmvc我也没那个能力重写,所以页面层只用jsp。
      详细分析下写一个mvc的流程:第一:我们也可以使用一个servlet将前端所有请求都接管到一个servlet中去(这里其实filter,servlet都可以实现,比如strus2采用filter接管,springmvc采用servlet,原理大同小异),第二:这个servlet是随着容器自启动,所以需要配置load-on-startup,然后我们在这个servlet的init方法里面可以扫描指定的包(扫描哪些包,可以通过servlet在web.xml中的init-param配置),加载一些注解并将注解配置的属性和类,方法对象的关系保存在一些map中,第三:当页面访问任意后端接口时,最终会经过doGet或者doPost(上层是service方法,为了方便不用service方法),我们可以在这两个方法中根据请求的url路径去找到对应的Controller类和Method对象,然后从请求中拿出参数传入方法需要的参数,得到方法执行的结果,最后根据方法里面指定的返回格式(@ResponseBody这种),或者请求转发,或者重定向做处理。废话不多说,直接上代码。

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
      <display-name>mymvc</display-name>
      <servlet>
        <servlet-name>dispacher</servlet-name>
        <servlet-class>com.rd.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>package</param-name>
            <param-value>com.rd.controller</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
      </servlet>
      <servlet-mapping>
        <servlet-name>dispacher</servlet-name>
        <url-pattern>/</url-pattern>
      </servlet-mapping>
    </web-app>
    package com.rd.servlet;
    
    import java.io.IOException;
    import java.lang.reflect.Method;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.regex.Pattern;
    
    import javax.servlet.ServletConfig;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.apache.commons.lang3.StringUtils;
    
    import com.rd.annotation.Path;
    import com.rd.annotation.RespJson;
    import com.rd.util.JsonUtil;
    import com.rd.util.MethodUtil;
    import com.rd.util.ScanClassUtil;
    
    /**
     * 类似springmvc的将请求全部纳入控制范围的servlet
     * @author rongdi
     * @date 2017年9月20日 下午1:58:26
     */
    public class DispatcherServlet extends HttpServlet {
        
        private static final long serialVersionUID = 1L;
        
        //path和Class的映射
        private final static Map<String, Class<?>> classMap = new HashMap<String, Class<?>>();
        
        //path和Method的映射
        private final static Map<String, Method> methodMap = new HashMap<String, Method>();
        
        //存放被@RespJson修饰的类
        private final static Set<Class<?>> classRespJsons = new HashSet<Class<?>>();
        
        //存放被@RespJson修饰的方法
        private final static Set<Method> methodRespJsons = new HashSet<Method>();
        
        @Override
        public void init(ServletConfig config) throws ServletException {
            System.out.println("---DispatcherServlet初始化开始---");
            //获取web.xml中配置的要扫描的包
            String basePackage = config.getInitParameter("package");
            //配置了多个包
            if (basePackage.indexOf(",")>0) {
                //按逗号进行分隔
                String[] packageNameArr = basePackage.split(",");
                for (String packageName : packageNameArr) {
                    add2ClassMap(packageName);
                }
            }else {
                add2ClassMap(basePackage);
            }
            System.out.println("----DispatcherServlet初始化结束---");
        }
        
        /**
         * 将被注解修饰的类
         * @param packageName
         */
        private void add2ClassMap(String packageName){
            Set<Class<?>> setClasses =  ScanClassUtil.getClasses(packageName);
            for (Class<?> clazz :setClasses) {
                String pathAttrValue = null;
                //判断类被注解修饰
                if (clazz.isAnnotationPresent(Path.class)) {
                    //获取path的Annotation的实例
                    Path pathInstance = clazz.getAnnotation(Path.class);
                    //获取Annotation的实例的value属性的值
                    pathAttrValue = pathInstance.value();
                    if (StringUtils.isNotEmpty(pathAttrValue)) {
                        pathAttrValue = handPathStr(pathAttrValue);
                        classMap.put(pathAttrValue, clazz);
                    }
                }
                if(clazz.isAnnotationPresent(RespJson.class)) {
                    classRespJsons.add(clazz);
                }
                //判断方法被注解修饰
                Method[] methods = clazz.getMethods();
                for(Method m:methods) {
                    //判断方法被注解修饰
                    if(m.isAnnotationPresent(Path.class)) {
                        //获取path的Annotation的实例
                        Path pathInstance = m.getAnnotation(Path.class);
                        //获取Annotation的实例的value属性的值
                        String methodPathValue = pathInstance.value();
                        if (StringUtils.isNotEmpty(methodPathValue)) {
                            methodPathValue = handPathStr(methodPathValue);
                            pathAttrValue = handPathStr(pathAttrValue);
                            methodMap.put(pathAttrValue+methodPathValue, m);
                        }
                    }
                    if(m.isAnnotationPresent(RespJson.class)) {
                        methodRespJsons.add(m);
                    }
                }
            }
        }
    
        /**
         * 处理一下路径,前面后面的斜杠
         * @param pathStr
         * @return
         */
        private String handPathStr(String pathStr) {
            if(pathStr.endsWith("/")) {
                pathStr = pathStr.substring(0,pathStr.length()-1);
            }
            if(!pathStr.startsWith("/")) {
                pathStr = "/"+pathStr;
            }
            return pathStr;
        }
        
        @Override
        public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            
            Map<String,String[]> params = request.getParameterMap();
            String url = request.getRequestURI();
            //当请求根路径是,请求转发到首页
            if("/".equals(url)) {
                request.getRequestDispatcher("/index.jsp").forward(request, response);
                return;
            }
            if("/favicon.ico".equals(url)) {
                return;
            }
            Method m = null;
            if(methodMap.containsKey(url)) {
                m = methodMap.get(url);
            } else {
                //这个过程,其实可以优化,如果存在通配符匹配,不用每次都循环匹配,可以缓存起来,第二次直接用,这里暂时忽略优化问题
                Set<String> urls = methodMap.keySet();
                for(String murl:urls) {
                    String reg = "^"+murl.replace("*", ".*?")+"$";
                    if(Pattern.matches(reg, url)) {
                        m = methodMap.get(murl);
                        break;
                    }
                }
            }
            if(m == null) {
                throw new ServletException("没有找到与路径:"+url+"对应的处理方法");
            }
            
            try {
                /**
                 * 这里需要获取参数名,jdk1.8之后可以直接直接反射获取,条件比较恶心,需要开启开关
                 * 如下直接使用javassist字节码工具类实现,也可以用asm等其他工具
                 */
                String[] paramNames = MethodUtil.getAllParamaterName(m);
                List<String> paramValues = new ArrayList<String>();
                for(String paramName:paramNames) {
                    if(params.get(paramName) == null) {
                        paramValues.add(null);
                    } else {
                        paramValues.add(params.get(paramName)[0]);
                    }
                    
                }
                /**
                 * 调用方法所在类的默认构造方法,生成执行方法的对象(springmvc里的这个对象是单例的,这里为了省事,每次都new一个出来),
                 * 然后执行方法,返回结果
                 */
                Class<?> cla = m.getDeclaringClass();
                Object result = m.invoke(cla.newInstance(), paramValues.toArray());
                /**
                 * 如果方法返回类型为 void,则该调用结果返回 null,如果返回值为void,则直接跳转到同路径的jsp页面上,
                 * 为了简单起见这里后缀写死.jsp,实际上springmvc是支持配置ViewResolver,
                 * 可以指定请求转发或者重定向所在的界面层的前缀和后缀。
                 */
                if(result == null) {
                    request.getRequestDispatcher(url+".jsp").forward(request, response);
                }
                /**
                 * 这里springmvc默认是请求转发到jsp
                 * 为了方便这里直接根据修饰返回类型的注解,确定用哪种方式序列化,
                 */
                if(classRespJsons.contains(cla) || methodRespJsons.contains(m)) {
                    result = JsonUtil.serialize(result);
                    //输出json到浏览器
                    response.getWriter().print(result);
                } else if(result.toString().startsWith("redirect:")) { //重定向
                    //去掉前缀就是重定向到的路径,实际上这里不严谨,应该加上一个项目的上下文路径
                    response.sendRedirect(result.toString().substring(9)+".jsp");
                } else { //请求转发
                    request.getRequestDispatcher(result.toString()+".jsp").forward(request, response);
                }
                
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            doGet(request, response);
        }
    
         public static void main(String[] args) {
                String url = "/test/a";
                String reg = "^/test/.*?$";
                System.out.println(Pattern.matches(reg, url));
                System.out.println(Long.class);
                        
            }
    }
    package com.rd.controller;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import com.rd.annotation.Path;
    import com.rd.annotation.RespJson;
    
    @Path(value="/test")
    public class TestController {
    
        @Path(value="/text")
        public void text() {
        }
        
        @Path(value="/redirect")
        public String redirect(String a) {
            
            return "redirect:/test/redirect";
        }
        
        @Path(value="/json")
        @RespJson
        public Map json(String a) {
            return new HashMap(){{put("a",a);}};
        }
        
    }
    package com.rd.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    public @interface Path {
        
        //访问的匹配路径
        String value();
       
    }
    package com.rd.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    public @interface RespJson {
        
    }
    package com.rd.util;
    
    import java.util.HashMap;
    
    import com.fasterxml.jackson.databind.DeserializationFeature;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    public class JsonUtil {
        
        private static ObjectMapper mapper;
    
        static {
            mapper = new ObjectMapper();
            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
                    false);
        }
        
        public static String serialize(Object obj) throws Exception {
    
            if (obj == null) {
                throw new IllegalArgumentException("obj should not be null");
            }
            return mapper.writeValueAsString(obj);
        }
        
        public static void main(String[] args) throws Exception {
            System.out.println(serialize(new HashMap(){{put("name","zhangsan");}}));
        }
    }
    package com.rd.util;
    import java.lang.reflect.Method;
    
    import javassist.ClassClassPath;
    import javassist.ClassPool;  
    import javassist.CtClass;  
    import javassist.CtMethod;  
    import javassist.Modifier;  
    import javassist.NotFoundException;  
    import javassist.bytecode.CodeAttribute;  
    import javassist.bytecode.LocalVariableAttribute;  
    import javassist.bytecode.MethodInfo;  
      
    /**
     * 使用javassist的方法工具
     * @author rongdi
     * @date 2017年9月20日 上午11:21:29
     */
    public class MethodUtil {  
      
        public static String[] getAllParamaterName(Method method)  
            throws NotFoundException {  
            Class<?> clazz = method.getDeclaringClass();  
            ClassPool pool = ClassPool.getDefault();  
            ClassClassPath classPath = new ClassClassPath(MethodUtil.class);  
            pool.insertClassPath(classPath);  
            CtClass clz = pool.get(clazz.getName());  
            CtClass[] params = new CtClass[method.getParameterTypes().length];  
            for (int i = 0; i < method.getParameterTypes().length; i++) {  
                params[i] = pool.getCtClass(method.getParameterTypes()[i].getName());  
            }  
            CtMethod cm = clz.getDeclaredMethod(method.getName(), params);  
            MethodInfo methodInfo = cm.getMethodInfo();  
            CodeAttribute codeAttribute = methodInfo.getCodeAttribute();  
            LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute  
                .getAttribute(LocalVariableAttribute.tag);  
            int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;  
            String[] paramNames = new String[cm.getParameterTypes().length];  
            for (int i = 0; i < paramNames.length; i++) {  
                paramNames[i] = attr.variableName(i + pos);  
            }  
            return paramNames;  
        }  
      
    }  
    package com.rd.util;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.Properties;
    
    /**
     * @author rongdi
     * @date 2017年9月16日 下午3:36:08
     */
    public class PropertyUtil {
    
        private static Properties props;
    
        synchronized static private void loadProps() {
            props = new Properties();
            InputStream in = null;
            
            try {
                try {
                    String path = getJarDir()+"/application.properties";
                    in = new FileInputStream(path);
                } catch(Exception e){
                    in = PropertyUtil.class.getClassLoader().getResourceAsStream("application.properties");
                }
                props.load(in);
            } catch (FileNotFoundException e) {
                //logger.error("application.properties文件未找到");
            } catch (IOException e) {
                //logger.error("出现IOException");
            } finally {
                try {
                    if (null != in) {
                        in.close();
                    }
                } catch (IOException e) {
                    //logger.error("application.properties文件流关闭出现异常");
                }
            }
        }
    
        public static String getProperty(String key) {
            if (null == props) {
                loadProps();
            }
            return props.getProperty(key);
        }
    
        public static String getProperty(String key, String defaultValue) {
            if (null == props) {
                loadProps();
            }
            return props.getProperty(key, defaultValue);
        }
    
        /**
         * 获取jar绝对路径
         * 
         * @return
         */
        public static String getJarPath() {
            File file = getFile();
            if (file == null)
                return null;
            return file.getAbsolutePath();
        }
    
        /**
         * 获取jar目录
         * 
         * @return
         */
        public static String getJarDir() {
            File file = getFile();
            if (file == null)
                return null;
            return getFile().getParent();
        }
    
        /**
         * 获取jar包名
         * 
         * @return
         */
        public static String getJarName() {
            File file = getFile();
            if (file == null)
                return null;
            return getFile().getName();
        }
    
        /**
         * 获取当前Jar文件
         * 
         * @return
         */
        private static File getFile() {
            // 关键是这行...
            String path = PropertyUtil.class.getProtectionDomain().getCodeSource()
                    .getLocation().getFile();
            try {
                path = java.net.URLDecoder.decode(path, "UTF-8"); // 转换处理中文及空格
            } catch (java.io.UnsupportedEncodingException e) {
                return null;
            }
            return new File(path);
        }
    }
    package com.rd.util;
    import java.io.File;
    import java.io.FileFilter;
    import java.io.IOException;
    import java.net.JarURLConnection;
    import java.net.URL;
    import java.net.URLDecoder;
    import java.util.Enumeration;
    import java.util.LinkedHashSet;
    import java.util.Set;
    import java.util.jar.JarEntry;
    import java.util.jar.JarFile;
    
    /**
     * 扫描字节码工具类
     * @author rongdi
     * @date 2017年9月19日 下午4:13:19
     */
    public class ScanClassUtil {
    
        /**
         * 从包package中获取所有的Class
         * @param pack
         * @return
         */
        public static Set<Class<?>> getClasses(String pack) {
    
            // 第一个class类的集合
            Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
            // 是否循环迭代
            boolean recursive = true;
            // 获取包的名字 并进行替换
            String packageName = pack;
            String packageDirName = packageName.replace('.', '/');
            // 定义一个枚举的集合 并进行循环来处理这个目录下的things
            Enumeration<URL> dirs;
            try {
                dirs = Thread.currentThread().getContextClassLoader().getResources(
                        packageDirName);
                // 循环迭代下去
                while (dirs.hasMoreElements()) {
                    // 获取下一个元素
                    URL url = dirs.nextElement();
                    // 得到协议的名称
                    String protocol = url.getProtocol();
                    // 如果是以文件的形式保存在服务器上
                    if ("file".equals(protocol)) {
                        System.err.println("file类型的扫描");
                        // 获取包的物理路径
                        String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                        // 以文件的方式扫描整个包下的文件 并添加到集合中
                        findAndAddClassesInPackageByFile(packageName, filePath,recursive, classes);
                    } else if ("jar".equals(protocol)) {
                        // 如果是jar包文件
                        // 定义一个JarFile
                        System.err.println("jar类型的扫描");
                        JarFile jar;
                        try {
                            // 获取jar
                            jar = ((JarURLConnection) url.openConnection())
                                    .getJarFile();
                            // 从此jar包 得到一个枚举类
                            Enumeration<JarEntry> entries = jar.entries();
                            // 同样的进行循环迭代
                            while (entries.hasMoreElements()) {
                                // 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件
                                JarEntry entry = entries.nextElement();
                                String name = entry.getName();
                                // 如果是以/开头的
                                if (name.charAt(0) == '/') {
                                    // 获取后面的字符串
                                    name = name.substring(1);
                                }
                                // 如果前半部分和定义的包名相同
                                if (name.startsWith(packageDirName)) {
                                    int idx = name.lastIndexOf('/');
                                    // 如果以"/"结尾 是一个包
                                    if (idx != -1) {
                                        // 获取包名 把"/"替换成"."
                                        packageName = name.substring(0, idx).replace('/', '.');
                                    }
                                    // 如果可以迭代下去 并且是一个包
                                    if ((idx != -1) || recursive) {
                                        // 如果是一个.class文件 而且不是目录
                                        if (name.endsWith(".class") && !entry.isDirectory()) {
                                            // 去掉后面的".class" 获取真正的类名
                                            String className = name.substring(
                                                    packageName.length() + 1, name
                                                            .length() - 6);
                                            try {
                                                // 添加到classes
                                                classes.add(Class.forName(packageName + '.'
                                                                + className));
                                            } catch (ClassNotFoundException e) {
                                                e.printStackTrace();
                                            }
                                        }
                                    }
                                }
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return classes;
        }
        
        /**
         * 以文件的形式来获取包下的所有Class
         * 
         * @param packageName
         * @param packagePath
         * @param recursive
         * @param classes
         */
        public static void findAndAddClassesInPackageByFile(String packageName,
                String packagePath, final boolean recursive, Set<Class<?>> classes) {
            // 获取此包的目录 建立一个File
            File dir = new File(packagePath);
            // 如果不存在或者 也不是目录就直接返回
            if (!dir.exists() || !dir.isDirectory()) {
                // log.warn("用户定义包名 " + packageName + " 下没有任何文件");
                return;
            }
            // 如果存在 就获取包下的所有文件 包括目录
            File[] dirfiles = dir.listFiles(new FileFilter() {
                // 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件)
                public boolean accept(File file) {
                    return (recursive && file.isDirectory())
                            || (file.getName().endsWith(".class"));
                }
            });
            // 循环所有文件
            for (File file : dirfiles) {
                // 如果是目录 则继续扫描
                if (file.isDirectory()) {
                    findAndAddClassesInPackageByFile(packageName + "."
                            + file.getName(), file.getAbsolutePath(), recursive,
                            classes);
                } else {
                    // 如果是java类文件 去掉后面的.class 只留下类名
                    String className = file.getName().substring(0,
                            file.getName().length() - 6);
                    try {
                        // 添加到集合中去
                        //classes.add(Class.forName(packageName + '.' + className));
                        classes.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className));  
                        } catch (ClassNotFoundException e) {
                        // log.error("添加用户自定义视图类错误 找不到此类的.class文件");
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    <properties>
            <project.deploy>deploy</project.deploy>
            <jackson.version>2.5.4</jackson.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>javax.servlet</groupId>
                <artifactId>javax.servlet-api</artifactId>
                <version>3.1.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.4</version>
            </dependency>
            <dependency>
                <groupId>org.javassist</groupId>
                <artifactId>javassist</artifactId>
                <version>3.21.0-GA</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-core</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-annotations</artifactId>
                <version>${jackson.version}</version>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>3.8.1</version>
                <scope>test</scope>
            </dependency>
    
        </dependencies>

    完整代码百度云地址(不要吐槽要放git啥的,我屌丝一个,不用那么高大上的东西):https://pan.baidu.com/s/1mi24Lbq

  • 相关阅读:
    逆元应用求组合数
    树的重心入门
    扫描线求面积的并,交
    涂抹果酱
    牧场的安排
    「SCOI2005」互不侵犯
    Network Coverage
    Linux命令传输文件
    VMware Workstation 与 Device/Credential Guard 不兼容.在禁用 Device/Credenti
    Springboot开启SpringSecurity
  • 原文地址:https://www.cnblogs.com/rongdi/p/8017041.html
Copyright © 2011-2022 走看看