在本文中,博主一步步地從servlet到controller層實現一個簡單的框架。經過此框架,咱們能夠像spring那樣使用如下基礎註解:css
觀看本文以前,你或許應該先了解如下內容:html
思考:前端
問題:java
攔截器如何找到使用了該註解的方法?包掃描?如何實現?git
分析:github
包掃描,就涉及IO流, 而File類能夠遞歸查詢其下面全部的文件,咱們web
能夠過濾一下:spring
這樣在攔截器攔截到請求路徑,咱們能夠進行匹配並調用該方法。apache
偷個懶:json
由於MVC設計模式,咱們通常把api接口都放在同一個包下,因此咱們能夠直接指定要掃描包,其它包就無論
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獲取其參數列表,再傳對應參數,就是不太好實現 } }
@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()); } }
經過1.0版,咱們初步實現遞歸掃描包下的全部controller,並能經過路徑映射實現訪問。但很明顯有至少如下問題:
針對以上2個問題,咱們在2.0版進行一下修改:
@XxgRequestBody
以及@XxgParam
區分參數從請求體拿或者從url的?後面拿。從而獲取前端傳來的數據ObjectMapper
進行不一樣類型參數的裝配,最後調用方法的invoke
實現帶參/不帶參的方法處理。/** * 用來存放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的註解 }
/** * 描述方法的類 */ @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; // 返回數據 }
/** * 描述參數的類 */ @Data @NoArgsConstructor @AllArgsConstructor public class ParameterDefinition { private Class paramClazz; // 參數類對象 private String paramName; // 參數名稱 private Object paramType; // 參數類型 private boolean isRequestBody; // 是不是獲取body中數據 }
/** * 用於存放請求路徑 與 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監聽/
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(); } }
@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(); } }
靜態資源也被攔截了
打開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.0版是博主本身思考並完成的。
2.0版是博主的小高老師給博主講了思路,寫出來後又看了小高老師的實現,而後綜合着完善的。