欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

自己写一个java的mvc框架吧(二)

程序员文章站 2022-05-19 19:27:53
自己写一个mvc框架吧(二) 自己写代码的习惯 写一个框架吧,如果这个框架会用到一些配置上的东西,我自己习惯是先不用考虑这个配置文件应该是怎样的,什么形式的,先用一个java对象(比如叫 Config.java ) 都给放进去。等到功能写的差不多了,需要考虑到使用配置文件了,就可以写一个工厂类,根据 ......

自己写一个mvc框架吧(二)

自己写代码的习惯

写一个框架吧,如果这个框架会用到一些配置上的东西,我自己习惯是先不用考虑这个配置文件应该是怎样的,什么形式的,先用一个java对象(比如叫 config.java) 都给放进去。等到功能写的差不多了,需要考虑到使用配置文件了,就可以写一个工厂类,根据不同的配置(可能是xml,可能是json,甚至是注解)把刚才说的 config.java 对象生成出来。

现在开始写~

我们先写url与method的映射关系

装模做样的分析一下

因为一个mvc的框架个人感觉主要做的事情就是通过http请求调用java中的方法。首先要做的就是怎样把一个请求地址和一个java中的方法绑定起来,使其形成一个对应关系。另外请求也是分请求类型的,比如get,post等等,所以还需要请求类型。

其次,要通过java的反射执行这个方法的话,还需要这个method的所属class的实例对象。

最后,因为这个方法是要通过http调用的,我们需要知道这个method中的入参有哪些,每个参数是什么类型的,之后才能从每一次的请求中找到相应的参数,并转换成为对应的java类型。所以我们还需要每个参数的参数名称。

最终我们需要的是:

  1. 一个url地址
  2. 对应的请求类型
  3. 一个method对象
  4. method所属class的实例对象
  5. method的入参参数名称
  6. method的入参参数类型,以class形式存在

创建一个描述映射的类 urlmethodmapping

import lombok.getter;
import lombok.setter;
import lombok.tostring;

import java.lang.reflect.method;

/**
 * 一个请求url到method的映射
 *
 * @author hjx
 */
@tostring
@setter
@getter
public class urlmethodmapping {

    /**
     * 请求地址
     */
    private string url;

    /**
     * 请求类型
     */
    private requesttype[] requesttypes;

    /**
     * 请求方法所属class实例
     */
    private object object;

    /**
     * method的所属class
     */
    private class objectclass;

    /**
     * url 对应的method
     */
    private method method;

    /**
     * method 的入参名称
     * 顺序要保持一致
     */
    private string[] paramnames;

    /**
     * method 的入参类型
     * 顺序要保持一致
     */
    private class[] paramclasses;

}

这里我没有写 getter和setter,是因为我用了一个叫做lombok的工具,很好用大家搜一下就知道怎么用了。

在上面的代码中有一个属性 requesttype[] requesttypes 这是一个枚举,主要是用来说明这个映射支持那些请求方式的。

现在将urlmethodmapping数据填充起来

我在这里写了一个工厂类,提供了一个方法来组装urlmethodmapping 这个对象:

/**
 * @param url           请求地址
 * @param requesttypes  http请求方式
 * @param objectclass   实例对象的class
 * @param method        url对应的方法
 * @param paramclasses  请求参数类型
 * @return
 */
public urlmethodmapping geturlmethodmapping(
        string url, requesttype[] requesttypes, class objectclass, method method, class[] paramclasses
) {
    assert.notnull(url, url + not_find);
    assert.notnull(requesttypes, request_type + not_find);
    assert.istrue(requesttypes.length > 0, request_type + not_find);
    assert.notnull(objectclass, class + not_find);
    assert.notnull(method, method + not_find);
    assert.notnull(paramclasses, param_types + not_find);

    //class实例化对象
    object object = objectfactory.getobject(objectclass);
    assert.notnull(object, "objectfactory.getobject() 获取失败!objectclass:" + objectclass.getname());
    //获取参数名称
    string[] paramnames = paramnamegetter.getparamnames(method);
    assert.notnull(paramnames, "paramnamegetter.getparamnames() 执行失败!method:" + method.getname());
    assert.istrue(paramnames.length == paramclasses.length, "方法名称取出异常 method:" + method.getname());
    //组装参数
    urlmethodmapping mapping = new urlmethodmapping();
    mapping.setmethod(method);
    mapping.seturl(url);
    mapping.setrequesttypes(requesttypes);
    mapping.setobject(object);
    mapping.setparamclasses(paramclasses);
    mapping.setobjectclass(objectclass);
    mapping.setparamnames(paramnames);
    return mapping;
}

在这个方法里,我用自己写的一个断言的工具类 assert 来校验参数是否是正确的,如果参数不正确的话就会抛出异常信息。这段代码基本上是这个样子:

public static void notnull(object obj, string msg) {
    if (obj == null) {
        throw new runtimeexception(msg);
    }
}

这段程序中还有两个对象:

1:objectfactory

是一个接口,主要用于通过class 来获取到实例化的对象,这里需要使用者自己实现。目的是为了和其他的 ioc框架 进行集成。比如在这个接口里可以通过从spring容器中获取实例化的对象。

2:paramnamegetter

还是一个接口,主要用于从method中获取入参的名称,我在这里提供了一个实现类,是通过 asm 来获取的。也可以再写一个通过注解获取参数名称的实现类。我在这里用的是asm

怎样使用asm获取参数名称呢?

​ 首先我们要添加asm的依赖

<dependency>
    <groupid>org.ow2.asm</groupid>
    <artifactid>asm</artifactid>
    <version>7.0</version>
</dependency>

这里我们主要用到asm中

1:classreader

public void accept(
    final classvisitor classvisitor, //一个classvisitor对象
    final int parsingoptions //在访问类时必须解析的属性原型
){
    ...    
}

这个类要求我们在构造函数中传入class的全限定名称,就是class.getname();

2:classvisitor.java

public methodvisitor visitmethod(
      final int access,//方法的访问标志
      final string name,//方法的名称
      final string descriptor,//方法的描述符
      final string signature,//方法的签名
      final string[] exceptions//方法的异常类的内部名称
){
    ...
}

这个方法会在执行classreader.accept()的时候被执行。返回值是一个methodvisitor

3:methodvisitor.java

public void visitlocalvariable(
      final string name,//局部变量的名称
      final string descriptor,//局部变量的类型描述符
      final string signature,//此局部变量的类型签名
      final label start,//对应于此局部变量范围的第一条指令
      final label end,//对应于此局部变量范围的最后一条指令
      final int index//局部变量的索引
){
    ...
}

这个方法会在methodvisitor.visitmethod()中被执行,没有返回值。我们需要的method的入参名称就是在这里获取的。

因为这两个类是将整个class的方法都扫描一遍,所以我们需要自己写两个类来继承它,在里面添加我们需要的逻辑。代码如下:

methodparamnameclassvisitor.java

import org.objectweb.asm.*;

import java.util.list;

/**
 * asm class访问器
 * 用于提取方法的实际参数名称
 *
 * @author hjx
 */
public class methodparamnameclassvisitor extends classvisitor {

    /**
     * 方法的参数名称
     */
    private list<string> paramnames;

    /**
     * 方法的名称
     */
    private string methodname;

    /**
     * 方法的参数类型
     */
    private class[] patamtypes;

    @override
    public methodvisitor visitmethod(
            int access, string name, string descriptor, string signature, string[] exceptions
    ) {
        methodvisitor visitmethod = super.visitmethod(access, name, descriptor, signature, exceptions);
        boolean samemethod = samemethod(name, methodname, descriptor, patamtypes);
        //如果是相同的方法, 执行取参数名称的操作
        if (samemethod) {
            methodparamnamemethodvisitor paramnamemethodvisitor = new methodparamnamemethodvisitor(
                    opcodes.asm4, visitmethod
            );
            paramnamemethodvisitor.paramnames = this.paramnames;
            paramnamemethodvisitor.paramlength = this.patamtypes.length;
            return paramnamemethodvisitor;
        }
        return visitmethod;
    }

    /**
     * 是否是相同的方法
     *
     * @param methodname
     * @param methodname2
     * @param descriptor
     * @param paramtypes
     * @return
     */
    private boolean samemethod(string methodname, string methodname2, string descriptor, class[] paramtypes) {
        //方法名相同
        assert.notnull(methodname);
        assert.notnull(methodname2);
        if (methodname.equals(methodname2)) {
            type[] argumenttypes = type.getargumenttypes(descriptor);
            //参数长度相同
            if (argumenttypes.length == paramtypes.length) {
                //参数类型相同
                for (int i = 0; i < argumenttypes.length; i++) {
                    if (!type.gettype(paramtypes[i]).equals(argumenttypes[i])) {
                        return false;
                    }
                }
                return true;
            }
        }
        return false;
    }

    /**
     * @param paramnames 取出的参数名称,传入一个空的集合
     * @param methodname 目标方法名称
     * @param patamtypes 目标方法的参数类型
     */
    public methodparamnameclassvisitor(list<string> paramnames, string methodname, class[] patamtypes) {
        super(opcodes.asm4);
        this.paramnames = paramnames;
        this.methodname = methodname;
        this.patamtypes = patamtypes;
    }


    /**
     * 禁止的操作
     * 无法正确使用,抛出异常
     *
     * @param api
     */
    public methodparamnameclassvisitor(int api) {
        super(api);
        throw new runtimeexception("不支持的操作, 请使用构造函数:methodparamnameclassvisitor(list<string> paramnames, int patamlength) !");
    }

}

/**
 * 用于取出方法的参数实际名称
 */
class methodparamnamemethodvisitor extends methodvisitor {

    /**
     * 方法的参数名称
     */
    list<string> paramnames;

    /**
     * 方法的参数长度
     */
    int paramlength;

    @override
    public void visitlocalvariable(
            string name, string descriptor, string signature, label start, label end, int index
    ) {
        super.visitlocalvariable(name, descriptor, signature, start, end, index);
        //index 为0 时, name是this
        //根据方法实际参数长度截取参数名称
        if (index != 0 && paramnames.size() < paramlength) {
            paramnames.add(name);
        }
    }

    public methodparamnamemethodvisitor(int api, methodvisitor methodvisitor) {
        super(api, methodvisitor);
    }
}

这个类里,因为继承父类之后必须要实现一个带参数的构造方法:

public methodparamnameclassvisitor(int api){
    super(api);
}

但是这个方法我不想用它,就在方法结束后抛了一个异常出来。并新写了一个构造方法:

/**
 * @param paramnames 取出的参数名称,传入一个空的集合
 * @param methodname 目标方法名称
 * @param patamtypes 目标方法的参数类型
 */
public methodparamnameclassvisitor(
    list<string> paramnames,
    string methodname,
    class[] patamtypes
) {
    super(opcodes.asm4);
    this.paramnames = paramnames;
    this.methodname = methodname;
    this.patamtypes = patamtypes;
}

其中paramnames传入一个空集合(不是null),在方法执行完毕后会在里面添加方法的入参名称。

这个类是这么用的(下面的代码就是上面说道的paramnamegetter的一个实现):

/**
 * 通过asm获取method的入参名称
 *
 * @param method
 * @return
 */
@override
public string[] getparamnames(method method) {
    assert.notnull(method);
    class aclass = method.getdeclaringclass();
    parameter[] parameters = method.getparameters();
    string methodname = method.getname();
    string classname = aclass.getname();
    classreader classreader = null;
    try {
        classreader = new classreader(classname);
    } catch (ioexception e) {
        e.printstacktrace();
    }
    class[] paramclasses = new class[parameters.length];
    for (int i = 0; i < paramclasses.length; i++) {
        paramclasses[i] = parameters[i].gettype();
    }
    //暂存参数名称
    list<string> paramnamelist = new arraylist<>();
    methodparamnameclassvisitor myclassvisitor = new methodparamnameclassvisitor(
            paramnamelist, methodname, paramclasses
    );
    classreader.accept(myclassvisitor, 0);
    return paramnamelist.toarray(new string[]{});
}

现在。映射关系urlmethodmapping中的数据就全部填充好了。

下一篇我们开始写转换参数,并通过反射执行method的代码。

拜拜~~~