怎么写呢?

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

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

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

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

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

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

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

现在开始写

定义一个注解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 :注解的保留策略,有这么几个取值范围:

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

3: @Target :作用目标,表示注解可以添加在什么地方,取值范围有:在此我向大家推荐一个架构学习交流裙。

交流学习裙号:687810532,里面会分享一些资深架构师录制的视频录像

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

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

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

首先这个注解可以加在 class 和 method 上。 加在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

这个方法接收两个参数,一个是包名 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

将文件夹中的全部符合条件的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

将 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,将扫描包之后获取到的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的代码摘出来,单独写一个方法:在此我向大家推荐一个架构学习交流裙。交流学习裙号:687810532,里面会分享一些资深架构师录制的视频录像

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

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

02-14 02:22