• 自己写一个java的mvc框架吧(五)


    自己写一个mvc框架吧(五)

    给框架添加注解的支持

    一段废话

    上一章本来是说这一章要写视图处理的部分,但是由于我在测试代码的时候需要频繁的修改配置文件太麻烦了。所以这一章先把支持注解的功能加上,这样就不需要经常地修改配置文件了。

    至于视图处理的地方,就还是先用json吧,找时间再写。

    项目地址在:https://github.com/hjx601496320/aMvc

    测试代码在:https://github.com/hjx601496320/amvc-test

    怎么写呢?

    因为在之前写代码的时候,我把每个类要做的事情分的比较清楚,所以在添加这个功能的时候写起来还是比较简单的,需要修改的地方也比较小。

    这一章里我们需要干的事情有:

    1. 定义一个注解,标识某一个class中的被添加注解的方法是一个UrlMethodMapping

    2. 修改配置文件,添加需要扫描的package

    3. 写一个方法,根据package中值找到其中所有的class

    4. UrlMethodMapping的工厂类UrlMethodMappingFactory中新加一个根据注解创建UrlMethodMapping的方法。

    5. Application中的init()方法中,根据是否开启注解支持,执行新的工厂类方法。

    6. 完了。

      多么简单呀~~~

    现在开始写

    定义一个注解Request

    关于怎样自定义注这件事,大家可以上网搜一下,比较简单。我这里只是简单的说一下。我先把代码贴出来:

    import com.hebaibai.amvc.RequestType;
    import java.lang.annotation.*;
    
    /**
     * 表示这个类中的,添加了@Request注解的method被映射为一个http地址。
     *
     * @author hjx
     */
    @Documented
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Request {
    
        /**
         * 请求类型
         * 支持GET,POST,DELETE,PUT
         *
         * @return
         */
        RequestType[] type() default {RequestType.GET, RequestType.POST, RequestType.DELETE, RequestType.PUT};
    
        /**
         * 请求地址
         * 添加在class上时,会将value中的值添加在其他方法上的@Request.value()的值前,作为基础地址。
         *
         * @return
         */
        String value() default "/";
    }
    
    

    定义一个注解,需要用到一下几个东西:

    1:@interface:说明这个类是一个注解。

    2:@Retention:注解的保留策略,有这么几个取值范围:

    代码 说明
    @Retention(RetentionPolicy.SOURCE) 注解仅存在于源码中
    @Retention(RetentionPolicy.CLASS) 注解会在class字节码文件中存在
    @Retention(RetentionPolicy.RUNTIME) 注解会在class字节码文件中存在,运行时可以通过反射获取到

    因为我们在程序中需要取到自定义的注解,所以使用:RetentionPolicy.RUNTIME

    3:@Target:作用目标,表示注解可以添加在什么地方,取值范围有:

    代码 说明
    @Target(ElementType.TYPE) 接口、类、枚举、注解
    @Target(ElementType.FIELD) 字段、枚举的常量
    @Target(ElementType.METHOD) 方法
    @Target(ElementType.PARAMETER) 方法参数
    @Target(ElementType.CONSTRUCTOR) 构造函数
    @Target(ElementType.LOCAL_VARIABLE) 局部变量
    @Target(ElementType.ANNOTATION_TYPE) 注解
    @Target(ElementType.PACKAGE)

    3:@Documented:这个主要是让自定义注解保留在文档中,没啥实际意义,一般都给加上。

    4:default:是给注解中的属性(看起来像是一个方法,也可能就是一个方法,但是我就是叫属性,略略略~~~)一个默认值。

    上面大致上讲了一下怎么定义一个注解,现在注解写完了,讲一下这个注解的用处吧

    首先这个注解可以加在classmethod上。加在class上的时候表示这个类中会有method将要被处理成为一个UrlMethodMapping,然后其中的value属性将作为这个class中所有UrlMethodMapping的基础地址,type属性不起作用加在method上的时候,就是说明这个method将被处理成一个UrlMethodMapping,注解的两个属性发挥其正常的作用。

    注解写完了,下面把配置文件改一改吧。

    修改框架的配置文件

    只需要添加一个属性就好了,修改完的配置文件这个样子:

    {
      "annotationSupport": true,
      "annotationPackage": "com.hebaibai.demo.web",
    //  "mapping": [
    //    {
    //      "url": "/index",
    //      "requestType": [
    //        "get"
    //      ],
    //      "method": "index",
    //      "objectClass": "com.hebaibai.demo.web.IndexController",
    //      "paramTypes": [
    //        "java.lang.String",
    //        "int"
    //      ]
    //    }
    //  ]
    }
    

    1:annotationSupport 值是true的时候表示开启注解。

    2:annotationPackage 表示需要扫描的包的路径。

    3:因为开了注解支持,为了防止重复注册 UrlMethodMapping,所以我把下面的配置注释掉了。

    写一个包扫描的方法

    这个方法需要将项目中jar文件文件夹下所有符合条件的class找到,会用到递归,代码在ClassUtils.java中,由三个方法构成,分别是:

    1:void getClassByPackage(String packageName, Set classes);

    这个方法接收两个参数,一个是包名packageName,一个是一个空的Set(不是null),在方法执行完毕会将包下的所有class填充进Set中。这里主要是判断了一下这个包中有那些类型的文件,并根据文件类型分别处理。

    注意:如果是jar文件的类型,获取到的filePath是这样的:

    file:/home/hjx/idea-IU/lib/idea_rt.jar!/com
    

    需要去掉头和尾,然后就可以吃了,鸡肉味!嘎嘣脆~~ 处理之后的是这个样子:

    /home/hjx/idea-IU/lib/idea_rt.jar
    

    下面是方法代码:

    /**
     * 从给定的报名中找出所有的class
     *
     * @param packageName
     * @param classes
     */
    @SneakyThrows({IOException.class})
    public static void getClassByPackage(String packageName, Set<Class> classes) {
        Assert.notNull(classes);
        String packagePath = packageName.replace(DOT, SLASH);
        Enumeration<URL> resources = ClassUtils.getClassLoader().getResources(packagePath);
        while (resources.hasMoreElements()) {
            URL url = resources.nextElement();
            //文件类型
            String protocol = url.getProtocol();
            String filePath = URLDecoder.decode(url.getFile(), CHARSET_UTF_8);
            if (TYPE_FILE.equals(protocol)) {
                getClassByFilePath(packageName, filePath, classes);
            }
            if (TYPE_JAR.equals(protocol)) {
                //截取文件的路径
                filePath = filePath.substring(filePath.indexOf(":") + 1, filePath.indexOf("!"));
                getClassByJarPath(packageName, filePath, classes);
            }
        }
    }
    

    2:void getClassByFilePath(String packageName, String filePath, Set classes)

    将文件夹中的全部符合条件的class找到,用到递归。需要将class文件的绝对路径截取成class的全限定名,代码这个样子:

    /**
     * 在文件夹中递归找出该文件夹中在package中的class
     *
     * @param packageName
     * @param filePath
     * @param classes
     */
    static void getClassByFilePath(
        String packageName,
        String filePath,
        Set<Class> classes
    ) {
        File targetFile = new File(filePath);
        if (!targetFile.exists()) {
            return;
        }
        if (targetFile.isDirectory()) {
            File[] files = targetFile.listFiles();
            for (File file : files) {
                String path = file.getPath();
                getClassByFilePath(packageName, path, classes);
            }
        } else {
            //如果是一个class文件
            boolean trueClass = filePath.endsWith(CLASS_MARK);
            if (trueClass) {
                //提取完整的类名
                filePath = filePath.replace(SLASH, DOT);
                int i = filePath.indexOf(packageName);
                String className = filePath.substring(i, filePath.length() - 6);
                //不是一个内部类
                boolean notInnerClass = className.indexOf("$") == -1;
                if (notInnerClass) {
                    //根据类名加载class对象
                    Class aClass = ClassUtils.forName(className);
                    if (aClass != null) {
                        classes.add(aClass);
                    }
                }
            }
        }
    }
    

    3:void getClassByJarPath(String packageName, String filePath, Set classes)

    jar文件中的全部符合条件的class找到。没啥说的,下面是代码:

    /**
     * 在jar文件中找出该文件夹中在package中的class
     *
     * @param packageName
     * @param filePath
     * @param classes
     */
    @SneakyThrows({IOException.class})
    static void getClassByJarPath(
        String packageName,
        String filePath,
        Set<Class> classes
    ) {
        JarFile jarFile = new URLJarFile(new File(filePath));
        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry jarEntry = entries.nextElement();
            String jarEntryName = jarEntry.getName().replace(SLASH, DOT);
            //在package下的class
            boolean trueClass = jarEntryName.endsWith(CLASS_MARK) && jarEntryName.startsWith(packageName);
            //不是一个内部类
            boolean notInnerClass = jarEntryName.indexOf("$") == -1;
            if (trueClass && notInnerClass) {
                String className = jarEntryName.substring(0, jarEntryName.length() - 6);
                System.out.println(className);
                //根据类名加载class对象
                Class aClass = ClassUtils.forName(className);
                if (aClass != null) {
                    classes.add(aClass);
                }
            }
        }
    }
    

    这样,获取包名下的class就写完了~

    修改UrlMethodMappingFactory

    这里新添加一个方法:

    List getUrlMethodMappingListByClass(Class aClass),将扫描包之后获取到的Class对象作为参数,返回一个UrlMethodMapping集合就好了。代码如下:

    /**
     * 通过解析Class 获取映射
     *
     * @param aClass
     * @return
     */
    public List<UrlMethodMapping> getUrlMethodMappingListByClass(Class<Request> aClass) {
        List<UrlMethodMapping> mappings = new ArrayList<>();
        Request request = aClass.getDeclaredAnnotation(Request.class);
        if (request == null) {
            return mappings;
        }
        String basePath = request.value();
        for (Method classMethod : aClass.getDeclaredMethods()) {
            UrlMethodMapping urlMethodMapping = getUrlMethodMappingListByMethod(classMethod);
            if (urlMethodMapping == null) {
                continue;
            }
            //将添加在class上的Request中的path作为基础路径
            String url = UrlUtils.makeUrl(basePath + "/" + urlMethodMapping.getUrl());
            urlMethodMapping.setUrl(url);
            mappings.add(urlMethodMapping);
        }
        return mappings;
    }
    
    /**
     * 通过解析Method 获取映射
     * 注解Request不存在时跳出
     *
     * @param method
     * @return
     */
    private UrlMethodMapping getUrlMethodMappingListByMethod(Method method) {
        Request request = method.getDeclaredAnnotation(Request.class);
        if (request == null) {
            return null;
        }
        Class<?> declaringClass = method.getDeclaringClass();
        String path = request.value();
        for (char c : path.toCharArray()) {
            Assert.isTrue(c != ' ', declaringClass + "." + method.getName() + "请求路径异常:" + path + " !");
        }
        return getUrlMethodMapping(
                path,
                request.type(),
                declaringClass,
                method,
                method.getParameterTypes()
        );
    }
    

    在这里校验了一下注解Request中的value的值,如果中间有空格的话会抛出异常。UrlUtils.makeUrl() 这个方法主要是将url中的多余”/”去掉,代码长这个样子:

    private static final String SLASH = "/";
    
    /**
     * 处理url
     * 1:去掉连接中相邻并重复的“/”,
     * 2:链接开头没有“/”,则添加。
     * 3:链接结尾有“/”,则去掉。
     *
     * @param url
     * @return
     */
    public static String makeUrl(@NonNull String url) {
        char[] chars = url.toCharArray();
        StringBuilder newUrl = new StringBuilder();
        if (!url.startsWith(SLASH)) {
            newUrl.append(SLASH);
        }
        for (int i = 0; i < chars.length; i++) {
            if (i != 0 && chars[i] == chars[i - 1] && chars[i] == '/') {
                continue;
            }
            if (i == chars.length - 1 && chars[i] == '/') {
                continue;
            }
            newUrl.append(chars[i]);
        }
        return newUrl.toString();
    }
    

    这样通过注解获取UrlMethodMapping的工厂方法就写完了,下面开始修改加载框架的代码。

    修改Application中的init

    这里因为添加了一种使用注解方式获取UrlMethodMapping的方法,所以新建一个方法:

    void addApplicationUrlMappingByAnnotationConfig(JSONObject configJson) 。在这里获取框架配置中的包名以及做一些配置上的校验,代码如下:

    /**
     * 使用注解来加载UrlMethodMapping
     *
     * @param configJson
     */
    private void addApplicationUrlMappingByAnnotationConfig(JSONObject configJson) {
        String annotationPackage = configJson.getString(ANNOTATION_PACKAGE_NODE);
        Assert.notNull(annotationPackage, ANNOTATION_PACKAGE_NODE + NOT_FIND);
        //获取添加了@Request的类
        Set<Class> classes = new HashSet<>();
        ClassUtils.getClassByPackage(annotationPackage, classes);
        Iterator<Class> iterator = classes.iterator();
        while (iterator.hasNext()) {
            Class aClass = iterator.next();
            List<UrlMethodMapping> mappings = urlMethodMappingFactory.getUrlMethodMappingListByClass(aClass);
            if (mappings.size() == 0) {
                continue;
            }
            for (UrlMethodMapping mapping : mappings) {
                addApplicationUrlMapping(mapping);
            }
        }
    }
    

    之后把先前写的读取json配置生成urlMappin的代码摘出来,单独写一个方法:

    void addApplicationUrlMappingByJsonConfig(JSONObject configJson),这样使代码中的每个方法的功能都独立出来,看起来比较整洁,清楚。代码如下:

    /**
     * 使用文件配置来加载UrlMethodMapping
     * 配置中找不到的话不执行。
     *
     * @param configJson
     */
    private void addApplicationUrlMappingByJsonConfig(JSONObject configJson) {
        JSONArray jsonArray = configJson.getJSONArray(MAPPING_NODE);
        if (jsonArray == null || jsonArray.size() == 0) {
            return;
        }
        for (int i = 0; i < jsonArray.size(); i++) {
            UrlMethodMapping mapping = urlMethodMappingFactory.getUrlMethodMappingByJson(jsonArray.getJSONObject(i));
            addApplicationUrlMapping(mapping);
        }
    }
    

    最后只要吧init()稍微修改一下就好了,修改完之后是这样的:

    /**
     * 初始化配置
     */
    @SneakyThrows(IOException.class)
    protected void init() {
        String configFileName = applicationName + ".json";
        InputStream inputStream = ClassUtils.getClassLoader().getResourceAsStream(configFileName);
        byte[] bytes = new byte[inputStream.available()];
        inputStream.read(bytes);
        String config = new String(bytes, "utf-8");
        //应用配置
        JSONObject configJson = JSONObject.parseObject(config);
    
        //TODO:生成对象的工厂类(先默认为每次都new一个新的对象)
        this.objectFactory = new AlwaysNewObjectFactory();
        //TODO:不同的入参名称获取类(当前默认为asm)
        urlMethodMappingFactory.setParamNameGetter(new AsmParamNameGetter());
        //通过文件配置加载
        addApplicationUrlMappingByJsonConfig(configJson);
        //是否开启注解支持
        Boolean annotationSupport = configJson.getBoolean(ANNOTATION_SUPPORT_NODE);
        Assert.notNull(annotationSupport, ANNOTATION_SUPPORT_NODE + NOT_FIND);
        if (annotationSupport) {
            addApplicationUrlMappingByAnnotationConfig(configJson);
        }
    }
    

    这里只是根据配置做了一下判断就好了。这样就写完了。

    最后

    是不是很简单啊~~~
    关于视图处理的部分看看下一章再写吧~~~

    最新修改一下

    没人看,不写了。等我先给我自己的小网站框架换成自己写的再说。
    中间这个框架可能会经常行的修改~

    拜拜~~

  • 相关阅读:
    高校宿舍管理系统详细需求分析说明书
    手把手教你实现"短信轰炸"
    爬取小说网站章节和小说语音播放
    报刊订阅管理系统的设计与实现
    宾馆客房管理系统的设计与实现
    Analysis of requirement specification of parking management system
    手把手教你微信好友头像形成指定的文字
    html、css/bootStrap、js/Jquery、ajax与json数据交互总结
    详细介绍idea实现javaweb项目登入注册(华东交通大学教务处信息管理系统)、模糊查询
    2020研究生考试各专业各学校的研究生录取分数和复试录取人数比率(各大学校研究生录取信息的官网直通车)
  • 原文地址:https://www.cnblogs.com/hebaibai/p/10369564.html
Copyright © 2020-2023  润新知