上一篇
自己寫一個mvc框架吧(二)
專案地址在:github.com/hjx60149632… 。
測試程式碼在:github.com/hjx60149632… 。
自己寫程式碼的習慣
寫一個框架吧,如果這個框架會用到一些配置上的東西,我自己習慣是先不用考慮這個配置檔案應該是怎樣的,什麼形式的,先用一個java物件(比如叫 Config.java) 都給放進去。等到功能寫的差不多了,需要考慮到使用配置檔案了,就可以寫一個工廠類,根據不同的配置(可能是xml,可能是json,甚至是註解)把剛才說的 Config.java 物件生成出來。
現在開始寫~
我們先寫URL與Method的對映關係
裝模做樣的分析一下
因為一個mvc的框架個人感覺主要做的事情就是通過http請求呼叫java中的方法。首先要做的就是怎樣把一個請求地址和一個java中的方法繫結起來,使其形成一個對應關係。另外請求也是分請求型別的,比如get,post等等,所以還需要請求型別。
其次,要通過java的反射執行這個方法的話,還需要這個Method的所屬Class的例項物件。
最後,因為這個方法是要通過http呼叫的,我們需要知道這個Method中的入參有哪些,每個引數是什麼型別的,之後才能從每一次的請求中找到相應的引數,並轉換成為對應的java型別。所以我們還需要每個引數的引數名稱。
最終我們需要的是:
- 一個URL地址
- 對應的請求型別
- 一個Method物件
- Method所屬Class的例項物件
- Method的入參引數名稱
- 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的程式碼。
拜拜~~~