前言
在本文中,博主一步步地从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 allClazz = new ArrayList<>(10); //存该包下所有用了注解的类
public List 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 Object annotation; // 注解
private String controllerUrlPath; // controller的path路径
private List 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 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 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 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 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 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 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 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 parameterDefinitions = methodDefinition.getParameterDefinitions();
List
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
,有如下配置:
default
org.apache.catalina.servlets.DefaultServlet
debug
0
listings
false
1
但是他并没有匹配servlet-mapping,即处理的路径,那么可以在我们项目的web.xml中做以下配置来处理静态资源:
DispatcherServlet
/
default
*.html
default
*.js
default
*.css
default
*.jpg
最后
一.本文其实主要做了以下两个操作
- 服务器启动时,扫描controller包,将符合我们预期的类、方法、参数装配到容器中。
- 前端访问服务器,获取容器中指定路径对应的方法
2.1 将访问参数按不同类型装配到参数列表中
2.2 执行对应方法
2.3 处理方法返回数据
二.参考说明
- 项目实现过程
1.0版是博主自己思考并完成的。
2.0版是博主的小高老师给博主讲了思路,写出来后又看了小高老师的实现,然后综合着完善的。
- 在写了文章后,博主对项目中不同类进行了解耦等操作,代码重构了一番,主要为了应付开闭原则、单一职责原则等。
- 代码:https://github.com/dengbenche...