自己寫一個mvc框架吧(五)

何白白發表於2019-02-13
上一篇

自己寫一個mvc框架吧(四)

自己寫一個mvc框架吧(五)

給框架新增註解的支援

一段廢話

上一章本來是說這一章要寫檢視處理的部分,但是由於我在測試程式碼的時候需要頻繁的修改配置檔案太麻煩了。所以這一章先把支援註解的功能加上,這樣就不需要經常地修改配置檔案了。

至於檢視處理的地方,就還是先用json吧,找時間再寫。

專案地址在:github.com/hjx60149632…

測試程式碼在:github.com/hjx60149632…

怎麼寫呢?

因為在之前寫程式碼的時候,我把每個類要做的事情分的比較清楚,所以在新增這個功能的時候寫起來還是比較簡單的,需要修改的地方也比較小。

這一章裡我們需要乾的事情有:

  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);
    }
}
複製程式碼

這裡只是根據配置做了一下判斷就好了。這樣就寫完了。

最後

是不是很簡單啊~~~ 關於檢視處理的部分看看下一章再寫吧~~~

最新修改一下

沒人看,不寫了。等我先給我自己的小網站框架換成自己寫的再說。 中間這個框架可能會經常行的修改~

拜拜~~

相關文章