每日一例 | 手寫controller、requestMapping注解,實現(xiàn)簡單請求

前言
今天我們還是繼續(xù)研究手寫web服務器,經過昨天一天,服務器這邊,我已經基本實現(xiàn)了controller注解和requestMapping注解。
服務器啟動的時候,會自動去掃描帶有controller注解的類,然后根據(jù)controller再去掃描requestMapping注解,最后生成一個key為url,value為方法的map。
當后端接收到前端請求后,根據(jù)請求地址調用相應的方法,如果地址不存在,就返回404。目前,調用方法這塊目前只實現(xiàn)了簡單方法的調用,帶入參的方法還沒實現(xiàn),也是同樣的思路,通過反射直接調用,然后將返回值寫入響應即可。
下面讓我們一起看下我是如何實現(xiàn)的。
Controller注解
首先定義一個注解,加了兩個元注解,一個是表明我們的注解是加在類上面的,一個表明我們的類要保留到運行時。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
String value() default "";
}
同時,我們還為注解指定了一個方法(我不知道這個應該叫屬性還是方法),目的是接收controller的名字。
RequestMapping注解
這個注解和上面的注解類似,因為這個類是要加到方法和類上的,所以這個注解我在target上多加了一個ElementType.METHOD。value()是用來接收url的,后期可能還有增加請求方法這個字段,這個后期再說。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
String value();
}
加了上面兩個注解的controller長這個樣子:
@Controller("test")
public class TestController {
@RequestMapping("/test")
public String testRequstMapping() {
return "hello syske-boot";
}
}
另一個
@Controller
public class Test2Controller {
@RequestMapping("/test2")
public String test2() {
return "test2";
}
}
包掃描器
這里才是關鍵了,所有的類掃描都是基于這里實現(xiàn)的。后期實現(xiàn)IoC和Aop也要用到。
現(xiàn)在controller的包路徑是寫死的,后面可以通過注解加在服務器主入口上,就和springboot差不多,這個也很好實現(xiàn)。
這里的邏輯也很簡單,就是掃描給定的包路徑,判斷類是否有controller注解,有就把它放進controllerSet。
然后再循環(huán)遍歷controllerSet,將加了@RequsetMapping注解的方法放進requestMappingMap。
public class SyskeBootContentScanHandler {
private static final Logger logger = LoggerFactory.getLogger(SyskeBootContentScanHandler.class);
private static Set<Class> controllerSet = Sets.newHashSet();
private static Map<String, Method> requestMappingMap = Maps.newHashMap();
private SyskeBootContentScanHandler() {}
/**
* 獲取請求方法Map
* @return
*/
public static Map<String, Method> getRequestMappingMap() {
return requestMappingMap;
}
/**
* 類加載器初始化
*
* @throws IOException
* @throws ClassNotFoundException
*/
public static void init() {
try {
// 掃描conttoller
scanPackage("io.github.syske.boot.controller", controllerSet);
// 掃描controller的RequestMapping
scanRequestMapping(controllerSet);
} catch (Exception e) {
logger.error("syske-boot 啟動異常:", e);
}
}
/**
* 掃描controller的RequestMapping
*
* @param controllerSet
*/
private static void scanRequestMapping(Set<Class> controllerSet) {
logger.info("start to scanRequestMapping, controllerSet = {}", controllerSet);
if (controllerSet == null) {
return;
}
controllerSet.forEach(aClass -> {
Method[] methods = aClass.getDeclaredMethods();
for (Method method : methods) {
RequestMapping annotation = method.getAnnotation(RequestMapping.class);
requestMappingMap.put(annotation.value(), method);
}
});
logger.info("scanRequestMapping end, requestMappingMap = {}", requestMappingMap);
}
/**
* 掃描指定的包名下的類
*
* @param packageName
* @param classSet
* @throws IOException
* @throws ClassNotFoundException
*/
private static void scanPackage(String packageName, Set<Class> classSet)
throws IOException, ClassNotFoundException {
logger.info("start to scanPackage, packageName = {}", packageName);
Enumeration<URL> classes = ClassLoader.getSystemResources(packageName.replace('.', '/'));
while (classes.hasMoreElements()) {
URL url = classes.nextElement();
File packagePath = new File(url.getPath());
if (packagePath.isDirectory()) {
String[] files = packagePath.list();
for (String fileName : files) {
String className = fileName.substring(0, fileName.lastIndexOf('.'));
String fullClassName = String.format("%s.%s", packageName, className);
classSet.add(Class.forName(fullClassName));
}
}
}
logger.info("scanPackage end, classSet = {}", classSet);
}
}
到這里,包掃描器的邏輯就完了。后期,隨著注解越來越多,考慮到兼容性,這塊的方法應該還需要進一步的抽象封裝。
SyskeRequestHandler調整
上面的掃描最終的目的都是為了響應請求的時候能夠更靈活,也是為這里服務的,所以需要對doDispatcher方法調整。
這里的邏輯也很簡單,就是根據(jù)請求頭中的地址,去匹配對應的方法,如果地址不存在就返回404。
如果方法存在,拿出對應的方法,反射調用即可。
現(xiàn)在是在doDispatcher方法內部實例化了controller,后面實現(xiàn)簡單IoC之后,就可以從我們的容器中直接獲取實例了。
private void init() throws IOException, IllegalParameterException {
this.syskeRequest = new SyskeRequest(socket.getInputStream());
this.syskeResponse = new SyskeResponse(socket.getOutputStream());
this.requestMappingMap = SyskeBootContentScanHandler.getRequestMappingMap();
}
public void doDispatcher() throws Exception{
logger.info("請求頭信息:{}", syskeRequest.getRequestHear());
logger.info("請求信息:{}", syskeRequest.getRequestAttributeMap());
String requestMapping = syskeRequest.getRequestHear().getRequestMapping();
if (requestMappingMap.containsKey(requestMapping)) {
Method method = requestMappingMap.get(requestMapping);
logger.debug("method:{}", method);
Class<?> declaringClass = method.getDeclaringClass();
Object o = declaringClass.newInstance();
Object invoke = method.invoke(o);
logger.info("invoke:{}", invoke);
syskeResponse.write(String.format("hello syskeCat, dateTime:%d\n result = %s", System.currentTimeMillis(), invoke));
} else {
syskeResponse.write(404, String.format("resources not found :%d", System.currentTimeMillis()));
}
socket.close();
}
我們看下請求效果,我們分別調用上面兩個controller接口試下,先看/test:

再看/test2:

result就是我們方法的返回值,說明我們的預期結果已經完美達成,后面就是好好打磨優(yōu)化了。
總結
其實昨天方法調用這塊還沒實現(xiàn),是剛剛寫的,總體來說很簡單,用到了反射的相關知識。下一步考慮先實現(xiàn)有參方法的調用問題,然后再實現(xiàn)IoC。總之,這個東西已經慢慢變成服務器該有的樣子,一切還是讓我覺得蠻意外的,所以大家有想法的時候,一定要努力去做,做了一切才有更多可能,我們一起加油吧!
下面是項目的開源倉庫,有興趣的小伙伴可以去看看,如果有想法的小伙伴,我真心推薦你自己動個手,自己寫一下,真的感覺不錯:
https://github.com/Syske/syske-boot

