共计 14518 个字符,预计需要花费 37 分钟才能阅读完成。
前言
在本文中,博主一步步地从 servlet 到 controller 层实现一个简略的框架。通过此框架,咱们能够像 spring 那样应用以下根底注解:
- @XxgController
- @XxgRequestMapping
- @XxgParam
- @XxgRequestBody
观看本文之前,你或者应该先理解以下内容:
- BeanUtils
- ObjectMapper
- Servlet 相干常识
思路:拦截器实现路由散发。利用注解?
思考:
- 拦截器能够在 servlet 之前拦挡所有申请门路
- 能够找到注解中门路与申请门路相匹配的那个办法
- 而后将 req,resp 转发给该办法来执行
问题:
拦截器如何找到应用了该注解的办法?包扫描?如何实现?
剖析:
包扫描,就波及 IO 流,而 File 类能够递归查问其上面所有的文件,咱们
能够过滤一下:
- 只有后缀名为.class 的文件,并获取其 className(包含包门路)
- 通过反射获取这个类,判断其是否有指定的注解进而再次过滤
这样在拦截器拦挡到申请门路,咱们能够进行匹配并调用该办法。
偷个懒:
因为 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,并能通过门路映射实现拜访。但很显著有至多以下问题:
- 执行办法时,办法不能有参数。不合乎业务需要
- 每次拜访,都要重复解决 Class 反射来找到门路映射的办法,效率低。
针对以上 2 个问题,咱们在 2.0 版进行一下批改:
- 将 controller、requestmapping 对应办法,办法对应参数的可能用到的相干信息寄存在一个容器中。在服务器首次启动时进行扫描,并拆卸到容器中。这样在每次拜访时,遍历这个容器,比 1.0 版的容器更不便。
- 定义参数类型,通过注解
@XxgRequestBody
以及@XxgParam
辨别参数从申请体拿或者从 url 的? 前面拿。从而获取前端传来的数据 - 通过
ObjectMapper
进行不同类型参数的拆卸,最初调用办法的invoke
实现带参 / 不带参的办法解决。
BeanDefinition
/**
* 用来寄存 controller 类的相干参数、办法等
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BeanDefinition {
private Class typeClazz; // 类对象
private String typeName; // 类名
private XxgController controller; // controller 注解
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 XxgRequestMapping requestMapping; // RequestMapping 的注解类
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.setController(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.setRequestMapping(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>
最初
一. 本文其实次要做了以下两个操作
- 服务器启动时,扫描 controller 包,将合乎咱们预期的类、办法、参数拆卸到容器中。
- 前端拜访服务器,获取容器中指定门路对应的办法
2.1 将拜访参数按不同类型拆卸到参数列表中
2.2 执行对应办法
2.3 解决办法返回数据
二. 参考阐明
1.0 版是博主本人思考并实现的。
2.0 版是博主的小高老师给博主讲了思路,写进去后又看了小高老师的实现,而后综合着欠缺的。
正文完