java的springMvc框架簡單實現

前言

在本文中,博主一步步地從servlet到controller層實現一個簡單的框架。經過此框架,咱們能夠像spring那樣使用如下基礎註解:css

  • @XxgController
  • @XxgRequestMapping
  • @XxgParam
  • @XxgRequestBody

觀看本文以前,你或許應該先了解如下內容:html

  • BeanUtils
  • ObjectMapper
  • Servlet相關知識

思路:攔截器實現路由分發。利用註解?

思考:前端

  1. 攔截器能夠在servlet以前攔截全部請求路徑
  2. 能夠找到註解中路徑與請求路徑相匹配的那個方法
  3. 而後將req,resp轉發給該方法來執行

問題:java

攔截器如何找到使用了該註解的方法?包掃描?如何實現?git

分析:github

包掃描,就涉及IO流, 而File類能夠遞歸查詢其下面全部的文件,咱們web

能夠過濾一下:spring

  1. 只要後綴名爲.class的文件,並獲取其className(包括包路徑)
  2. 經過反射獲取這個類,判斷其是否有指定的註解進而再次過濾

這樣在攔截器攔截到請求路徑,咱們能夠進行匹配並調用該方法。apache

偷個懶:json

由於MVC設計模式,咱們通常把api接口都放在同一個包下,因此咱們能夠直接指定要掃描包,其它包就無論

一.掃描類1.0版的實現

public class FileScanner {
 private final String packetUrl = "com.dbc.review.controller";
 private final ClassLoader classLoader = FileScanner.class.getClassLoader();
 private List<Class> allClazz = new ArrayList<>(10); //存該包下全部用了註解的類
​
 public List<Class> getAllClazz(){
   return this.allClazz;
 }
​
 public String getPacketUrl(){
   return this.packetUrl;
 }
​
 // 查詢全部使用了給定註解的類
 // 遞歸掃描包,若是掃描到class,則調用class處理方法來收集想要的class
 public void loadAllClass(String packetUrl) throws Exception{
     String url = packetUrl.replace(".","/");
     URL resource = classLoader.getResource(url);
     if (resource == null) {
        return;
    }
     String path = resource.getPath();
     File file = new File(URLDecoder.decode(path, "UTF-8"));
     if (!file.exists()) {
        return;
     }
     if (file.isDirectory()){
     File[] files = file.listFiles();
     if (files == null) {
        return;
     }
     for (File f : files) {
         String classname = f.getName().substring(0, f.getName().lastIndexOf("."));
         if (f.isDirectory()) {
            loadAllClass(packetUrl + "." + classname);
         }
         if (f.isFile() && f.getName().endsWith(".class")) {
             Class clazz = Class.forName(packetUrl + "." + classname);
             dealClass( clazz);
         }
    }
     }
 }
​
 private void dealClass(Class clazz) {
 if ((clazz.isAnnotationPresent(Controller.class))) {
 allClazz.add(clazz);
 }
 }
​
 // 真正使用的時候,根據請求路徑及請求方法來獲取處理的方法
 public boolean invoke(String url,String requestMethod) {
     for (Class clazz : allClazz){
     Controller controller = (Controller) clazz.getAnnotation(Controller.class);
     Method[] methods = clazz.getDeclaredMethods();
     for (Method method : methods) {
         RequestMapping requestMapping = method.getDeclaredAnnotation(RequestMapping.class);
         if (requestMapping == null) {
         continue;
         }
         for (String m : requestMapping.methods()) {
         m = m.toUpperCase();
         if (!m.toUpperCase().equals(requestMethod.toUpperCase())) {
         continue;
         }
         StringBuilder sb = new StringBuilder();
         String urlItem = sb.append(controller.url()).append(requestMapping.url()).toString();
         if (urlItem.equals(url)) {
         // 獲取到用於處理此api接口的方法
         try {
        //                            method.getGenericParameterTypes() // 能夠根據此方法來判斷該方法須要傳哪些參數
         method.invoke(clazz.newInstance());
         return true;
         } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
         e.printStackTrace();
         return false;
         }
     }
     }
     }
     }
     return false;
 }
​
 @Test
 public void test() throws Exception {
     // 1. 在Filter的靜態代碼塊中實例化
     FileScanner fileScanner = new FileScanner();
     // 2. 啓動掃描
     fileScanner.loadAllClass(fileScanner.getPacketUrl());
     // 3. 攔截到請求後,調用此方法來執行
     // 若該包下沒有定義post請求的/test/post 的處理方法,則返回false
     // 執行成功返回true
     fileScanner.invoke("/test/post","post");
     // 4. 執行失敗,返回false,則拋出405 方法未定義。
    ​
     // 最後 :對於controller的傳參,本類未實現
     //       暫時想到:根據method獲取其參數列表,再傳對應參數,就是不太好實現
     }
}

TestController

@Controller(url = "/test")
public class TestController {
​
 @RequestMapping(url = "/get",methods = "GET")
 public void get(){
 System.out.println(111);
 }
 @RequestMapping(url = "/post",methods = {"POST","get"})
 public void post(){
 System.out.println(22);
 }
​
 public void test(HttpServletRequest req, HttpServletResponse res){
 System.out.println(req.getPathInfo());
 }
}

掃描類2.0版

經過1.0版,咱們初步實現遞歸掃描包下的全部controller,並能經過路徑映射實現訪問。但很明顯有至少如下問題:

  1. 執行方法時,方法不能有參數。不符合業務需求
  2. 每次訪問,都要反覆處理Class反射來找到路徑映射的方法,效率低。

針對以上2個問題,咱們在2.0版進行一下修改:

  1. 將controller、requestmapping對應方法,方法對應參數的可能用到的相關信息存放在一個容器中。在服務器初次啓動時進行掃描,並裝配到容器中。這樣在每次訪問時,遍歷這個容器,比1.0版的容器更方便。
  2. 定義參數類型,經過註解@XxgRequestBody以及@XxgParam區分參數從請求體拿或者從url的?後面拿。從而獲取前端傳來的數據
  3. 經過ObjectMapper進行不一樣類型參數的裝配,最後調用方法的invoke實現帶參/不帶參的方法處理。

BeanDefinition

/**
 * 用來存放controller類的相關參數、方法等
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BeanDefinition {
 private Class typeClazz; // 類對象
 private String typeName; // 類名
 private Object annotation; // 註解
 private String controllerUrlPath; // controller的path路徑
 private List<MethodDefinition> methodDefinitions; // 帶有RequestMapping的註解
}

MethodDefinition

/**
 * 描述方法的類
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MethodDefinition {
 private Class parentClazz; // 所屬父類的class
 private Method method; // 方法
 private String methodName; // 方法名
 private Object annotation; // 註解類
 private String requestMappingUrlPath; // url
 private String[] allowedRequestMethods; // allowedRequestMethods
 private List<ParameterDefinition> parameterDefinitions;  // 參數列表
 private Object result;  // 返回數據
}

ParameterDefinition

/**
 * 描述參數的類
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ParameterDefinition {
 private Class paramClazz; // 參數類對象
 private String paramName; // 參數名稱
 private Object paramType; // 參數類型
 private boolean isRequestBody; // 是不是獲取body中數據
}

單例模式的容器

賦予掃描包及根據uri獲取對應方法的方法

/**
 * 用於存放請求路徑 與 controller對應關係的類
 * 設計成單例模型
 */
public class RequestPathContainer {
 private static List<BeanDefinition> requestList = new ArrayList<>();
 private static final ClassLoader classLoader = RequestPathContainer.class.getClassLoader();
 private static volatile RequestPathContainer instance = null;
​
 public static RequestPathContainer getInstance() {
     if (instance == null) {
        synchronized(RequestPathContainer.class){
            if (instance == null) {
                instance = new RequestPathContainer();
            }
         }
     }
     return instance;
 }
​
 private RequestPathContainer() {
​
 }
​
 public List<BeanDefinition> getRequestList() {
    return requestList;
 }
​
 // 掃描包
 public void scanner(String packetUrl) throws UnsupportedEncodingException, ClassNotFoundException {
     String url = packetUrl.replace(".", "/");
     URL resource = classLoader.getResource(url);
     if (resource == null) {
         return;
     }
     String path = resource.getPath();
     File file = new File(URLDecoder.decode(path, "UTF-8"));
     if (!file.exists()) {
         return;
     }
     if (file.isDirectory()){
     File[] files = file.listFiles();
     if (files == null) {
        return;
     }
     for (File f : files) {
     if (f.isDirectory()) {
        scanner(packetUrl + "." + f.getName());
     }
     if (f.isFile() && f.getName().endsWith(".class")) {
         String classname = f.getName().replace(".class", ""); // 去掉.class後綴名
         Class clazz = Class.forName(packetUrl + "." + classname);
         dealClass(clazz);
         }
     }
     }
 }
​
 // 篩選包中的類,並添加到List中
 private void dealClass(Class clazz) {
     if (!clazz.isAnnotationPresent(XxgController.class)) {
     // 沒有controller註解
        return;
     }
     List<MethodDefinition> methodDefinitions = new ArrayList<>();
     Method[] methods = clazz.getDeclaredMethods();
     for (Method method : methods) {
         // 方法轉 方法描述類
         MethodDefinition methodDefinition = convertMethodToMethodDefinition(method, clazz);
         if (methodDefinition != null) {
            methodDefinitions.add(methodDefinition);
         }
         }
         if (methodDefinitions.size() == 0) {
            return;
         }
         // 設置類描述類
         BeanDefinition beanDefinition = convertBeanToBeanDefinition(clazz, methodDefinitions);
         requestList.add(beanDefinition);
    }
​
 // 根據uri 和 請求方法 獲取執行方法
 public MethodDefinition getMethodDefinition(String uri, String method) {
     for (BeanDefinition beanDefinition: requestList) {
     if (!uri.contains(beanDefinition.getControllerUrlPath())) {
        continue;
     }
     List<MethodDefinition> methodDefinitions = beanDefinition.getMethodDefinitions();
     for (MethodDefinition methodDefinition: methodDefinitions) {
     StringBuilder sb = new StringBuilder().append(beanDefinition.getControllerUrlPath());
     sb.append(methodDefinition.getRequestMappingUrlPath());
     if (!sb.toString().equals(uri)) {
     continue;
     }
     String[] allowedRequestMethods = methodDefinition.getAllowedRequestMethods();
     for (String str : allowedRequestMethods) {
     if (str.toUpperCase().equals(method.toUpperCase())) {
     // 請求路徑 與 請求方法 均知足,返回該方法描述類
     return methodDefinition;
     }
     }
     }
     }
     return null;
 }
​
 /**
 * 將controller類 轉換爲 類的描述類
 */
 private BeanDefinition convertBeanToBeanDefinition(Class clazz, List<MethodDefinition> methodDefinitions) {
     BeanDefinition beanDefinition = new BeanDefinition();
     beanDefinition.setTypeName(clazz.getName());
     beanDefinition.setTypeClazz(clazz);
     XxgController controller = (XxgController) clazz.getAnnotation(XxgController.class);
     beanDefinition.setAnnotation(controller);
     beanDefinition.setControllerUrlPath(controller.value());
     beanDefinition.setMethodDefinitions(methodDefinitions);// 增長方法體
     return beanDefinition;
 }
​
 /**
 * 將方法 轉換爲 方法描述類
 */
 private MethodDefinition convertMethodToMethodDefinition(Method method, Class clazz) {
 if (!method.isAnnotationPresent(XxgRequestMapping.class)) {
 // 沒有RequestMapping註解
 return null;
 }
 method.setAccessible(true);
 Parameter[] parameters = method.getParameters();
 // 設置參數描述類
 List<ParameterDefinition> parameterDefinitions = new ArrayList<>();
 for ( Parameter parameter : parameters) {
 ParameterDefinition parameterDefinition = convertParamToParameterDefinition(parameter);
 parameterDefinitions.add(parameterDefinition);
 }
 // 設置方法描述類
 MethodDefinition methodDefinition = new MethodDefinition();
 methodDefinition.setParameterDefinitions(parameterDefinitions);  // 增長參數列表
 methodDefinition.setMethod(method);
 methodDefinition.setMethodName(method.getName());
 methodDefinition.setResult(method.getReturnType());
 XxgRequestMapping requestMapping = method.getAnnotation(XxgRequestMapping.class);
 methodDefinition.setRequestMappingUrlPath(requestMapping.value());
 methodDefinition.setAnnotation(requestMapping);
 methodDefinition.setAllowedRequestMethods(requestMapping.methods());
 methodDefinition.setParentClazz(clazz);
 return methodDefinition;
 }
​
 /**
 * 將參數 轉換爲 參數描述類
 */
 private ParameterDefinition convertParamToParameterDefinition(Parameter parameter) {
 ParameterDefinition parameterDefinition = new ParameterDefinition();
 if ( parameter.isAnnotationPresent(XxgParam.class)) {
 parameterDefinition.setParamName(parameter.getAnnotation(XxgParam.class).value());
 } else {
 parameterDefinition.setParamName(parameter.getName());
 }
 parameterDefinition.setParamClazz(parameter.getType());
 parameterDefinition.setParamType(parameter.getType());
 parameterDefinition.setRequestBody(parameter.isAnnotationPresent(XxgRequestBody.class));
 return parameterDefinition;
 }
​
}

全局servlet

不使用攔截器,仍然使用servlet來進行路由分發。此servlet監聽/

public class DispatcherServlet extends HttpServlet {
 private ObjectMapper objectMapper = new ObjectMapper();
​
 @Override
 protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
​
 // 編碼設置
 resp.setContentType("text/json;charset=utf-8");
 RequestPathContainer requestPathContainer = RequestPathContainer.getInstance();
 MethodDefinition methodDefinition = requestPathContainer.getMethodDefinition(req.getRequestURI(), req.getMethod());
​
 if (methodDefinition == null) {
 resp.setStatus(404);
 sendResponse(R.failed("請求路徑不存在"), req, resp);
 return;
 }
​
 List<ParameterDefinition> parameterDefinitions = methodDefinition.getParameterDefinitions();
 List<Object> params = new ArrayList<>(parameterDefinitions.size());
 for (ParameterDefinition parameterDefinition : parameterDefinitions) {
 try {
 Object value = dealParam(parameterDefinition, req, resp);
 params.add(value);
 } catch (ParamException e) {
 resp.setStatus(404);
 sendResponse(R.failed(e.getMessage()), req, resp);
 return ;
 }
 }
​
 try {
 Object result = methodDefinition.getMethod().invoke(methodDefinition.getParentClazz().newInstance(), params.toArray());
 sendResponse(result, req, resp);
 } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
 e.printStackTrace();
 sendResponse(e.getMessage(), req, resp);
 }
​
 }
​
 /**
 * 處理參數
 * @param parameterDefinition
 * @param req
 * @param resp
 */
 private Object dealParam(ParameterDefinition parameterDefinition, HttpServletRequest req, HttpServletResponse resp) throws ParamException, IOException {
 Object value;
 String data = "";
 if (parameterDefinition.isRequestBody()) {
 // 從請求體(request的輸入流)中獲取數據
 data = getJsonString(req);
 } else if (parameterDefinition.getParamType() == HttpServletRequest.class) {
 return req;
 } else if (parameterDefinition.getParamType() == HttpServletResponse.class) {
 return resp;
 } else if (isJavaType(parameterDefinition)) {
 // 從url中取出參數
 data = req.getParameter(parameterDefinition.getParamName());
 if(data == null) {
 throw new ParamException("服務器沒法拿到請求數據,請檢查請求頭等");
 }
 } else {
 // 將請求url中的參數封裝成對象
 try {
 Object obj = parameterDefinition.getParamClazz().newInstance();
 ConvertUtils.register(new DateConverter(), Date.class);
 BeanUtils.populate(obj, req.getParameterMap());
 return obj;
 } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
 throw new ParamException("未找到參數'" + parameterDefinition.getParamName() + "'對應的值");
 }
 }
 try {
 value = objectMapper.readValue(data, parameterDefinition.getParamClazz());
 } catch (JsonProcessingException e) {
 String errMsg = "參數'" + parameterDefinition.getParamName() +
 "'須要'" + parameterDefinition.getParamType() +
 "類型";
 throw new ParamException(errMsg);
 }
 return value;
 }
​
 private void sendResponse(Object result, HttpServletRequest req, HttpServletResponse resp) throws IOException {
 if (result == null) {
 return;
 }
 resp.setContentType("text/json;charset=utf-8");
 objectMapper.writeValue(resp.getWriter(), result);
 }
​
 /**
 * 判斷參數是不是普通類型
 * @return
 */
 private boolean isJavaType(ParameterDefinition parameterDefinition) {
 Object[] javaTypes = MyJavaType.getJavaTypes();
 for (Object item : javaTypes) {
 if (item.equals(parameterDefinition.getParamClazz())) {
 return true;
 }
 }
 return false;
 }
​
 /**
 * 獲取請求頭的json字符串
 */
 private String getJsonString(HttpServletRequest req) throws IOException {
 BufferedReader br = new BufferedReader(new InputStreamReader(req.getInputStream(), "utf-8"));
 char[] chars = new char[1024];
 int len;
 StringBuilder sb = new StringBuilder();
 while ((len = br.read(chars)) != -1) {
 sb.append(chars, 0, len);
 }
 return sb.toString();
 }
}

servletcontext監聽器初始化容器

@Override
 public void contextInitialized(ServletContextEvent servletContextEvent) {
 RequestPathContainer requestPathContainer = RequestPathContainer.getInstance();
 String configClassName = servletContextEvent.getServletContext().getInitParameter("config");
 Class appListenerClass = null;
 try {
 appListenerClass = Class.forName(configClassName);
 XxgScanner xxgScanner = (XxgScanner)appListenerClass.getAnnotation(XxgScanner.class);
 if (xxgScanner != null) {
 try {
 requestPathContainer.scanner(xxgScanner.value()); // 掃描controller類,初始化List
 } catch (UnsupportedEncodingException | ClassNotFoundException e) {
 e.printStackTrace();
 }
 }
 } catch (ClassNotFoundException e) {
 e.printStackTrace();
 }
 }

遺留的問題

靜態資源也被攔截了

處理靜態資源

default servlet

打開tomcat的conf/web.xml文件,能夠發現tomcat默認有個default servlet,有以下配置:

<servlet>
 <servlet-name>default</servlet-name>
 <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
 <init-param>
 <param-name>debug</param-name>
 <param-value>0</param-value>
 </init-param>
 <init-param>
 <param-name>listings</param-name>
 <param-value>false</param-value>
 </init-param>
 <load-on-startup>1</load-on-startup>
 </servlet>

可是他並無匹配servlet-mapping,即處理的路徑,那麼能夠在咱們項目的web.xml中作如下配置來處理靜態資源:

<!--  將全局攔截器的匹配/* 改爲 / 。必須-->
<!--  /表示只處理其餘的servlet不能匹配的路徑-->
<servlet-mapping>
 <servlet-name>DispatcherServlet</servlet-name>
 <url-pattern>/</url-pattern>
 </servlet-mapping>
<!--  靜態資源-->
 <servlet-mapping>
 <servlet-name>default</servlet-name>
 <url-pattern>*.html</url-pattern>
 </servlet-mapping>
 <servlet-mapping>
 <servlet-name>default</servlet-name>
 <url-pattern>*.js</url-pattern>
 </servlet-mapping>
 <servlet-mapping>
 <servlet-name>default</servlet-name>
 <url-pattern>*.css</url-pattern>
 </servlet-mapping>
 <servlet-mapping>
 <servlet-name>default</servlet-name>
 <url-pattern>*.jpg</url-pattern>
 </servlet-mapping>

最後

一.本文其實主要作了如下兩個操做

  1. 服務器啓動時,掃描controller包,將符合咱們預期的類、方法、參數裝配到容器中。
  2. 前端訪問服務器,獲取容器中指定路徑對應的方法
    2.1 將訪問參數按不一樣類型裝配到參數列表中
    2.2 執行對應方法
    2.3 處理方法返回數據

二.參考說明

  • 項目實現過程

1.0版是博主本身思考並完成的。
2.0版是博主的小高老師給博主講了思路,寫出來後又看了小高老師的實現,而後綜合着完善的。

  • 在寫了文章後,博主對項目中不一樣類進行了解耦等操做,代碼重構了一番,主要爲了應付開閉原則、單一職責原則等。
  • 代碼:https://github.com/dengbenche...

傳送門

下一節:AutoWired屬性上使用的簡單實現

相關文章
相關標籤/搜索