1. 程式人生 > >自己寫一個java的mvc框架吧(二)

自己寫一個java的mvc框架吧(二)

.get 支持 請求方式 實例 list amt 但是 提取方法 事情

自己寫一個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的代碼。

拜拜~~~

自己寫一個java的mvc框架吧(二)