自己寫一個java的mvc框架吧(五)
自己寫一個mvc框架吧(五)
給框架添加註解的支援
一段廢話
上一章本來是說這一章要寫檢視處理的部分,但是由於我在測試程式碼的時候需要頻繁的修改配置檔案 ,太麻煩了 。所以這一章先把支援註解的功能加上,這樣就不需要經常地修改配置檔案了。
至於檢視處理的地方,就還是先用json吧,找時間再寫。
專案地址在:https://github.com/hjx601496320/aMvc 。
測試程式碼在:https://github.com/hjx601496320/amvc-test 。
怎麼寫呢?
因為在之前寫程式碼的時候,我把每個類要做的事情分的比較清楚,所以在新增這個功能的時候寫起來還是比較簡單的,需要修改的地方也比較小。
這一章裡我們需要乾的事情有:
-
定義一個註解,標識某一個class中的被添加註解的方法是一個UrlMethodMapping 。
-
修改配置檔案,新增需要掃描的package 。
-
寫一個方法,根據package 中值找到其中所有的class 。
-
在UrlMethodMapping 的工廠類UrlMethodMappingFactory 中新加一個根據註解建立UrlMethodMapping 的方法。
-
在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 :註解的保留策略,有這麼幾個取值範圍:
程式碼 | 說明 |
---|---|
@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 :是給註解中的屬性(看起來像是一個方法,也可能就是一個方法,但是我就是叫屬性,略略略~~~)一個預設值。
上面大致上講了一下怎麼定義一個註解,現在註解寫完了,講一下這個註解的用處吧 。
首先這個註解可以加在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的程式碼摘出來,單獨寫一個方法:
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); } }
這裡只是根據配置做了一下判斷就好了。這樣就寫完了。
最後
是不是很簡單啊~~~
關於檢視處理的部分看看下一章再寫吧~~~
完
拜拜~~