自己写一个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 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);
}
}
这里只是根据配置做了一下判断就好了。这样就写完了。
最后
是不是很简单啊~~~
关于视图处理的部分看看下一章再写吧~~~
最新修改一下
没人看,不写了。等我先给我自己的小网站框架换成自己写的再说。
中间这个框架可能会经常行的修改~
完
拜拜~~
自己写一个java的mvc框架吧(五)的更多相关文章
- 自己写一个java的mvc框架吧(四)
自己写一个mvc框架吧(四) 写一个请求的入口,以及初始化框架 上一章写了获取方法的入参,并根据入参的参数类型进行数据转换.这时候,我们已经具备了通过反射调用方法的一切必要条件.现在我们缺少一个htt ...
- 自己写一个java的mvc框架吧(三)
自己写一个mvc框架吧(三) 根据Method获取参数并转换参数类型 上一篇我们将url与Method的映射创建完毕,并成功的将映射关系创建起来了.这一篇我们将根据Method的入参参数名称.参数类型 ...
- 自己写一个java的mvc框架吧(二)
自己写一个mvc框架吧(二) 自己写代码的习惯 写一个框架吧,如果这个框架会用到一些配置上的东西,我自己习惯是先不用考虑这个配置文件应该是怎样的,什么形式的,先用一个java对象(比如叫 Config ...
- 自己写一个java的mvc框架吧(一)
自己写一个mvc框架吧(一) 目录 自己写一个mvc框架吧(一) 自己写一个mvc框架吧(二) 自己写一个mvc框架吧(三) 自己写一个mvc框架吧(四) 写之前的一些废话 废话 1 (总是要先随便说 ...
- 自己动手写一个简单的MVC框架(第一版)
一.MVC概念回顾 路由(Route).控制器(Controller).行为(Action).模型(Model).视图(View) 用一句简单地话来描述以上关键点: 路由(Route)就相当于一个公司 ...
- 自己动手写一个简单的MVC框架(第二版)
一.ASP.NET MVC核心机制回顾 在ASP.NET MVC中,最核心的当属“路由系统”,而路由系统的核心则源于一个强大的System.Web.Routing.dll组件. 在这个System.W ...
- AsMVC:一个简单的MVC框架的Java实现
当初看了<从零开始写一个Java Web框架>,也跟着写了一遍,但当时学艺不精,真正进脑子里的并不是很多,作者将依赖注入框架和MVC框架写在一起也给我造成了不小的困扰.最近刚好看了一遍sp ...
- Summer——从头开始写一个简易的Spring框架
Summer--从头开始写一个简易的Spring框架 参考Spring框架实现一个简易类似的Java框架.计划陆续实现IOC.AOP.以及数据访问模块和事务控制模块. ...
- 用Python写一个简单的Web框架
一.概述 二.从demo_app开始 三.WSGI中的application 四.区分URL 五.重构 1.正则匹配URL 2.DRY 3.抽象出框架 六.参考 一.概述 在Python中,WSGI( ...
随机推荐
- Android开发教程 - 使用Data Binding(三)在Activity中的使用
本系列目录 使用Data Binding(一)介绍 使用Data Binding(二)集成与配置 使用Data Binding(三)在Activity中的使用 使用Data Binding(四)在Fr ...
- OCP考试062题库出现大量新题-19
choose three Which three statements are true about Oracle Data Pump? A) Oracle Data Pump export and ...
- 为什么transform对行内元素不生效
注:赶时间的同学可直接下拉到底,看结论. 我使用transform对一个元素进行位移,代码如下: <div class="box"> <span>今天你吃了 ...
- XAMPP中MySQL无法启动解决办法
如图 问题出在mysql的路径上,其实报错已经讲得听清楚了 预期应该是这样 结果却是这样 所以解决办法当然就是修改这个路径,出现这个报错原因大多因为之前电脑装过mysql,所以电脑默认启动是原来的my ...
- 如何开启windows的linux子系统
win10一周年纪念版 1607的版本增加了bash,bash,bash,windows的shell中可以直接运行bash了. 下面说一下配置步骤: 1.设置 —更新和安全—针对开发人员,选择开发人 ...
- Oracle 扩展表空间大小的几种方式
环境:windows操作系统 增加表空间大小的四种方法Meathod1:给表空间增加数据文件 ALTER TABLESPACE app_data ADD DATAFILE 'D:\ORACLE\PRO ...
- Tornado初探
Tornado 是 FriendFeed 使用的可扩展的非阻塞式 web 服务器及其相关工具的开源版本.这个 Web 框架看起来有些像web.py 或者 Google 的 webapp,不过为了能有效 ...
- (转)python中的selectors模块
原文:https://www.cnblogs.com/yinheyi/p/8127871.html https://www.rddoc.com/doc/Python/3.6.0/zh/library/ ...
- 图片训练:使用卷积神经网络(CNN)识别手写数字
这篇文章中,我们将使用CNN构建一个Tensorflow.js模型来分辨手写的数字.首先,我们通过使之“查看”数以千计的数字图片以及他们对应的标识来训练分辨器.然后我们再通过此模型从未“见到”过的测试 ...
- Java之装饰模式
1.装饰模式的理解 在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能.它是通过创建一个包装对象,也就是装饰来包裹真实的对象. 2.装饰模式由4种角色组成 (1)抽象构件(Componen ...