本身寫一個mvc框架吧(一)java
項目地址在:github.com/hjx60149632… 。git
測試代碼在:github.com/hjx60149632… 。github
寫一個框架吧,若是這個框架會用到一些配置上的東西,我本身習慣是先不用考慮這個配置文件應該是怎樣的,什麼形式的,先用一個java對象(好比叫 Config.java) 都給放進去。等到功能寫的差很少了,須要考慮到使用配置文件了,就能夠寫一個工廠類,根據不一樣的配置(多是xml,多是json,甚至是註解)把剛纔說的 Config.java 對象生成出來。web
如今開始寫~json
由於一個mvc的框架我的感受主要作的事情就是經過http請求調用java中的方法。首先要作的就是怎樣把一個請求地址和一個java中的方法綁定起來,使其造成一個對應關係。另外請求也是分請求類型的,好比get,post等等,因此還須要請求類型。api
其次,要經過java的反射執行這個方法的話,還須要這個Method的所屬Class的實例對象。mvc
最後,由於這個方法是要經過http調用的,咱們須要知道這個Method中的入參有哪些,每一個參數是什麼類型的,以後才能從每一次的請求中找到相應的參數,並轉換成爲對應的java類型。因此咱們還須要每一個參數的參數名稱。app
最終咱們須要的是:框架
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的工具,很好用你們搜一下就知道怎麼用了。ide
在上面的代碼中有一個屬性 RequestType[] requestTypes 這是一個枚舉,主要是用來講明這個映射支持那些請求方式的。
我在這裏寫了一個工廠類,提供了一個方法來組裝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的依賴
<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的代碼。
拜拜~~~