手寫Spring之控制器controller實現mvc控制
寫在前面
最近學習了一下spring的相關內容,所以也就想要照貓畫虎地記錄和實現一下spring的框架,通過閱讀這些也希望能夠消除對Spring框架的恐懼,其實細心閱讀框架也很容易理解。
mini-spring儘量實現spring的核心功能。文章寫得很倉促,可能不是很全面,在全部完成之後我們再來完善表達吧,見諒~
項目的源碼我放在了github上:源碼地址
我會在這裏整理文章的系列目錄:
- 從頭開始實現一個小型spring框架——手寫Spring之實現SpringBoot啓動
- 從頭開始實現一個小型spring框架——手寫Spring之集成Tomcat服務器
- 從頭開始實現一個小型spring框架——控制器controller的實現
- 從頭開始實現一個小型spring框架——實現Bean管理(IOC與DI)
一、容器內對請求的處理過程
我們在上一篇博客裏講了,容器內具體請求的處理流程對於容器是一個黑盒,根據具體的實現而有所不同。下面我們就對典型的容器內部的處理流程進行簡單的總結和敘述。
1.1 請求典型流程
先看這麼一張圖片:
熟悉Servlet開發流程的都知道,我們需要在web.xml中配置我們的Servlet路徑和請求路徑,或者使用註解進行配置,纔可以完成URL和Servlet之間的映射。
我們把URI到Servlet的映射配置到Xml裏,當一個http請求到達服務器時,服務器會獲取這個請求的URI,然後去Web.xml中查找,通過映射表找到對應的Servlet,並把這個請求轉發到對應的Servlet,Servlet處理完後,再把結響應回去。這就是整個業務的處理流程了。
1.2 存在的問題
- 服務器調度的問題
配置集中,大而且雜亂,維護成本高 - 需要多次實現Servlet接口
1.3 Spring的改進
Spring對這個問題的改進,是引入的一個總管一樣的方式進行管理,也就是我們非常熟悉的DispatcherServlet,用來受理所有的業務。
其中的差異可以對比下面這張圖片
tomcat統一將請求發送給DispatcherServlet,再由DispatcherServlet將這些請求派發給具體的Mapping Handler,處理完畢後返回。其中Servlet接口僅實現了一次。並且通過註解的方式進行配置,更簡化了我們的實現流程,分散配置,業務邏輯也就變得清晰明確。
二、mvc實現
2.1 變化後的包結構
本次實現新增加了
- framework模塊
-
core包中增加ClassScanner實現包掃描
-
增加handler包實現反射獲取類信息
-
mvc包增加註解
- Controller註解標誌類
- RequestMapping註解方法
- RequestParam註解參數
-
修改servlet包實現全局
/
路徑下的uri攔截
- test模塊
- 增加controller實現測試請求
2.2 framework模塊實現DispatcherServlet和反射獲取類信息
core包下的ClassScanner類
package com.qcby.core;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* @author kevinlyz
* @ClassName ClassScanner
* @Description 通過類加載器獲取目錄下的類列表
* @Date 2019-06-08 17:26
**/
public class ClassScanner {
public static List<Class<?>> scannClasses(String packageName) throws IOException, ClassNotFoundException {
List<Class<?>> classes = new ArrayList<>();
String path = packageName.replace(".","/");
//獲取類加載器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Enumeration<URL> resources = classLoader.getResources(path);
while (resources.hasMoreElements()){
URL resource = resources.nextElement();
//處理資源類型是jar包的情況
if (resource.getProtocol().contains("jar")){
JarURLConnection jarURLConnection =
(JarURLConnection) resource.openConnection();
String jarFilePath = jarURLConnection.getJarFile().getName();
classes.addAll(getClassFromJar(jarFilePath,path));
}else{
// TODO: 處理jar包以外的情況
}
}
return classes;
}
/**
* @Author kevinlyz
* @Description 從jar包中獲取資源
* @Date 17:37 2019-06-08
* @Param
* @return List<Class<?>>
**/
public static List<Class<?>> getClassFromJar(String jarFilePath,String path) throws IOException, ClassNotFoundException {
List<Class<?>> classes = new ArrayList<>();
//獲取jar實例
JarFile jarFile = new JarFile(jarFilePath);
Enumeration<JarEntry> jarEntrys = jarFile.entries();
while (jarEntrys.hasMoreElements()){
JarEntry jarEntry = jarEntrys.nextElement();
//獲取類路徑名 如 com/qcby/test/Test.class
String entryName = jarEntry.getName();
//獲取的
if (entryName.startsWith(path)&&entryName.endsWith(".class")){
//路徑替換
String classFullName = entryName.replace("/",".").substring(0,entryName.length()-6);
//反射獲取類信息並添加至list
classes.add(Class.forName(classFullName));
}
}
return classes;
}
}
scannClasses之後會在MiniApplication中被調用,用於獲取類列表。
MiniApplication類,添加獲取類列表和反射調用。
package com.qcby.starter;
import com.qcby.core.ClassScanner;
import com.qcby.web.handler.HandlerManagger;
import com.qcby.web.server.TomcatServer;
import java.util.List;
/**
* @author kevinlyz
* @ClassName MiniApplication
* @Description 框架的入口類
* @Date 2019-06-04 19:21
**/
public class MiniApplication {
public static void run(Class<?> cls,String[] args){
System.out.println("Hello mini-spring application!");
TomcatServer tomcatServer = new TomcatServer(args);
try {
tomcatServer.startServer();
List<Class<?>> classList = ClassScanner.scannClasses(cls.getPackage().getName());
classList.forEach(it->System.out.println(it.getName()));
HandlerManagger.resolveMappingHandler(classList);
} catch (Exception e) {
e.printStackTrace();
}
}
}
HandlerManager類
package com.qcby.web.handler;
import com.qcby.web.mvc.Controller;
import com.qcby.web.mvc.RequestMapping;
import com.qcby.web.mvc.RequestParam;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;
/**
* @author kevinlyz
* @ClassName HandlerManagger
* @Description 反射獲取類信息
* @Date 2019-06-08 18:34
**/
public class HandlerManagger {
public static List<MappingHandler> mappingHandlerList = new ArrayList<>();
public static void resolveMappingHandler(List<Class<?>> classList){
for (Class<?> cls : classList){
if (cls.isAnnotationPresent(Controller.class)){
presentHandlerFromController(cls);
}
}
}
private static void presentHandlerFromController(Class<?> cls) {
//獲取方法
Method[] methods= cls.getDeclaredMethods();
for (Method method : methods){
if(!method.isAnnotationPresent(RequestMapping.class))
continue;
//獲取uri路徑
String uri = method.getDeclaredAnnotation(RequestMapping.class).value();
List<String> paramList = new ArrayList<>();
//獲取參數值
for (Parameter parameter : method.getParameters()){
if (parameter.isAnnotationPresent(RequestParam.class)){
paramList.add(parameter.getDeclaredAnnotation(RequestParam.class).value());
}
}
String[] params= paramList.toArray(new String[paramList.size()]);
MappingHandler mappingHandler = new MappingHandler(uri,method,cls,params);
HandlerManagger.mappingHandlerList.add(mappingHandler);
}
}
}
MappingHandler
package com.qcby.web.handler;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @author kevinlyz
* @ClassName MappingHandler
* @Description 包含uri,類的方法信息,類信息和參數
* @Date 2019-06-08 18:32
**/
public class MappingHandler {
private String uri;
private Method method;
private Class<?> controller;
private String[] args;
public MappingHandler(String uri, Method method, Class<?> controller, String[] args) {
this.uri = uri;
this.method = method;
this.controller = controller;
this.args = args;
}
public boolean handle(ServletRequest req, ServletResponse res) throws IllegalAccessException, InstantiationException, InvocationTargetException, IOException {
String reqUri = ((HttpServletRequest)req).getRequestURI();
if (!this.uri.equals(reqUri))
return false;
//相等則調用Handler的resolveMappingHandler方法,實例化並返回
Object[] parameters = new Object[args.length];
for(int i=0;i<args.length;i++){
parameters[i] = req.getParameter(args[i]);
}
Object ctl = controller.newInstance();
Object response = method.invoke(ctl,parameters);
res.getWriter().println(response.toString());
return true;
}
}
mvc包下
Controller自定義註解
package com.qcby.web.mvc;
import java.lang.annotation.*;
/**
* @Author kevinlyz
* @Description 控制器註解,添加在Controller上
* @Date 17:10 2019-06-08
**/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Controller {
}
RequestMapping自定義註解
package com.qcby.web.mvc;
import java.lang.annotation.*;
/**
* @Author kevinlyz
* @Description 映射註解,添加在方法上
* @Date 17:11 2019-06-08
**/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequestMapping {
String value();
}
RequestParam自定義註解
package com.qcby.web.mvc;
import java.lang.annotation.*;
/**
* @Author kevinlyz
* @Description 參數註解,添加在方法參數上
* @Date 17:10 2019-06-08
**/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestParam {
String value();
}
server包下的TomcatServer(修改攔截uri爲 /
,並將請求轉發至DispatcherServlet)
package com.qcby.web.server;
import com.qcby.web.servlet.DispatcherServlet;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.Tomcat;
/**
* @author kevinlyz
* @ClassName TomcatServer
* @Description 集成Tomcat服務器,將請求轉發至DispatcherServlet
* @Date 2019-06-05 13:10
**/
public class TomcatServer {
private Tomcat tomcat;
private String[] agrs;
public TomcatServer(String[] agrs) {
this.agrs = agrs;
}
public void startServer() throws LifecycleException {
//實例化tomcat
tomcat = new Tomcat();
tomcat.setPort(9999);
tomcat.start();
//實例化context容器
Context context = new StandardContext();
context.setPath("");
context.addLifecycleListener(new Tomcat.FixContextListener());
DispatcherServlet servlet = new DispatcherServlet();
Tomcat.addServlet(context,"dispatcherServlet",servlet).setAsyncSupported(true);
//添加映射
context.addServletMappingDecoded("/","dispatcherServlet");
tomcat.getHost().addChild(context);
//設置常駐線程防止tomcat中途退出
Thread awaitThread = new Thread("tomcat_await_thread."){
@Override
public void run() {
TomcatServer.this.tomcat.getServer().await();
}
};
//設置爲非守護線程
awaitThread.setDaemon(false);
awaitThread.start();
}
}
servlet包:
package com.qcby.web.servlet;
import com.qcby.web.handler.HandlerManagger;
import com.qcby.web.handler.MappingHandler;
import javax.servlet.*;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
/**
* @author kevinlyz
* @ClassName TestServlet
* @Description 處理請求,請求攔截和匹配,若不存在對應uri則直接返回
* @Date 2019-06-05 13:28
**/
public class DispatcherServlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
for (MappingHandler mappingHandler : HandlerManagger.mappingHandlerList){
try {
if (mappingHandler.handle(req,res)){
return;
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
至此,framework核心的請求攔截就處理完成了
2.3 test模塊測試
我們在test模塊下新建一個UserController類用於測試我們的請求過程。
package com.qcby.controller;
import com.qcby.web.mvc.Controller;
import com.qcby.web.mvc.RequestMapping;
import com.qcby.web.mvc.RequestParam;
import java.lang.annotation.Retention;
/**
* @author kevinlyz
* @ClassName UserController
* @Description 測試請求
* @Date 2019-06-08 17:14
**/
@Controller
public class UserController {
@RequestMapping("/test")
public String test(@RequestParam("name") String name,@RequestParam("desc") String desc){
System.out.println("test訪問了!");
return "hello controller!";
}
}
同樣gradle build
java -jar test/build/libs/test-1.0-SNAPSHOT.jar
打開我們的瀏覽器,輸入http://localhost:9999/test
看到輸出的hello controller!
深藏功與名!!!
三、小結
今天我們對SpringMvc的核心功能進行了實現,通過使用包掃描和反射機制獲取到註解的信息,並進行實例化,最終實現請求的攔截和相應。
(寫的不是很詳細,改日再補)
test的Application向framework模塊的MiniApplication傳遞類信息,MiniApplication調用ClassScanner進行包掃描,通過類加載器掃描包中的類路徑(其中對jar包的信息做了進一步處理),並返回classList類列表至MiniApplication;進而使用HandlerManagger獲取類信息,並通過HandlerManager進行組裝。
TomcatServer則更改其攔截的請求uri路徑爲/
,以攔截所有通過tomcat的請求,並轉發至DispatcherServlet,DispatcherServlet的service方法進行uri匹配,匹配成功則調用Handler的resolveMappingHandler方法,實例化並返回響應信息。
然後我們又通過test模塊書寫了UserController進行測試,成功返回hello controller的字符串。至此我們的mvc的請求控制功能就完全實現了,仔細想想是不是很簡單呢~~