这一章先把支持注解的功能加上,这样就不需要经常地修改配置文件了。
至于视图处理的地方,就还是先用json吧,找时间再写。
项目地址在:https://github.com/hjx601496320/aMvc 。
测试代码在:https://github.com/hjx601496320/amvc-test 。
怎么写呢?
因为在之前写代码的时候,我把每个类要做的事情分的比较清楚,所以在添加这个功能的时候写起来还是比较简单的,需要修改的地方也比较小。
这一章里我们需要干的事情有:
多么简单呀~~~
现在开始写
定义一个注解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); } }