手动写个类似的Spring MVC框架试试
Spring MVC是个经得起考验的一个成熟框架,在企业内也是一个很多开发者选择的框架。
SpringMVC是一种基于Java,实现了Web MVC设计模式,请求驱动类型的轻量级Web框架,即使用了MVC架构模式的思想,将Web层进行职责解耦。基于请求驱动指的就是使用请求-响应模型,框架的目的就是帮助我们简化开发,SpringMVC也是要简化我们日常Web开发。
MVC设计模式
MVC设计模式的任务是将包含业务数据的模块与显示模块的视图解耦。这是怎样发生的?在模型和视图之间引入重定向层可以解决问题。此重定向层是控制器,控制器将接收请求,执行更新模型的操作,然后通知视图关于模型更改的消息。
概念很复杂,不管他先,但是我们现在知道Spring MVC通过注解可以分发请求。我们从这里入手,大概思路就是,一个HttpServlet接受所有的请求,然后根据路由,判断具体通知那个方法执行,从而达到类似的功能。
OK,那么我们现在首先要有一个接受所有请求的HttpServlet,我们定义一个BaseServlet,并继承HttpServlet;
/** * 请求访问入口(所有请求都会来到这里进行分发) * @author zhanglr * */ @WebServlet(name = "BaseServlet", urlPatterns = "/") public final class BaseServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.service(req, resp); } } }
我们先不去管service()的具体实现,我们现在有一个总的HttpServlet了,所有请求都会被拦截在这里,那么具体通知哪些方法呢,首先,我们需要有注解,类似Spring MVC的@RequestMapping、@PostMapping等;
那么我们现在定义两个注解,一个是定义在类上的注解,一个是定义具体方法的注解;
/** * 类注解映射,映射使用的类 * * @author zhanglr * */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE }) public @interface RequestType { String url(); /** * 请求方式,get或post,大小写不敏感 * @return */ String type(); }
/** * 方法注解映射,映射调用的方法 * @author zhanglr * */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface RequestMethod { String name() default ""; }
好了,我们现在有两个注解,一个是注解在类上的,也是路由请求地址的前一段地址,我们以模块来区分;第二个注解是注解在方法上的,也就是路由请求地址的后面一段地址,两段地址的意义我是用 【模块/功能】来组合;
解析:@RequestType中有两个属性,type是请求方式;url是路由地址,也是上面所有我习惯用的模块地址;
Ok,我们现在将这两个注解用起来,类似Spring MVC,我们先来创建一个UserController;
/** * 用户逻辑控制器 * @author zhanglr * */ @RequestType(type = "POST", url = "/user") public class UserController { private UserService userService = new UserService(); @RequestMethod(name = "/login.do") public IActionResult login(HttpServletRequest request, HttpServletResponse response) { // 登录逻辑 return ErrorCodeResponse.Success.getSuccess("success"); } /** * 用户注册 * @param request * @param response * @return */ @RequestMethod(name = "/register.do") public IActionResult registerUser(HttpServletRequest request, HttpServletResponse response) { UserRegisterRequest userRegisterRequest = ClassUtil.getObjectFromResquest(request, UserRegisterRequest.class); if (Types.isEmpty(userRegisterRequest.getAccount())) { return ErrorCodeResponse.AccountIsNull.getError(); } if (Types.isEmpty(userRegisterRequest.getPassword())) { return ErrorCodeResponse.PasswordIsNull.getError(); } if (Types.isEmpty(userRegisterRequest.getAgainPwd())) { return ErrorCodeResponse.ParamIsNull.getError("二次密码"); } return userService.registerUser(userRegisterRequest); } )
以上新建了一个用户逻辑控制器,@RequestType即模块功能地址为“/user”,请求方式为“Post”,用户类中定义了两个方法,分别是登录注册,@RequestMethod分别注解在两个方法中,类似登录功能即@RequestMethod(name = "/login")。那么我们预想中的请求登录地址就是“/user/login.do”,同样,注册地址就是“/user/register.do”;
OK,我们现在有SpringMVC的Controller了。其中有一些工具类待会再贴出来吧。
返回我们的BaseServlet,现在需要思考的就是接受到的请求,怎样准备的通知对应的方法,我们通过请求地址匹配注解上的内容即可;
/** * 请求访问入口(所有请求都会来到这里进行分发) * @author zhanglr * */ @WebServlet(name = "BaseServlet", urlPatterns = "/") public final class BaseServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.service(req, resp); req.setCharacterEncoding("utf-8"); resp.setContentType("text/html"); resp.setCharacterEncoding("utf-8"); String contextPath = req.getContextPath(); String requestUri = req.getRequestURI(); // 去除contextPath为真实请求路径 String realUri = requestUri.substring(contextPath.length(), requestUri.length()); // 获取入口包下的所有controller类 List<Class<?>> classes = ClassUtil.getAllClassByPackageName(this.getClass().getPackage()); for (Class<?> currClass : classes) { // 获取当前类的注解 RequestType requestType = currClass.getAnnotation(RequestType.class); String methodType = req.getMethod().toLowerCase().trim(); // 当前类注解不为空,请求路径以该注解url开头且请求类型为注解type则获取该类 if (requestType != null && requestType.type().toLowerCase().trim().equals(methodType) && realUri.contains(requestType.url()) && realUri.startsWith(requestType.url())) { // 获取当前类的所有方法 Method[] methods = currClass.getDeclaredMethods(); for (Method currMethod : methods) { // 获取当前方法的注解 RequestMethod requestMethod = currMethod.getAnnotation(RequestMethod.class); // 当前方法注解不为空且请求路径以该注解name结尾,则获取该方法 if (requestMethod != null && realUri.contains(requestMethod.name()) && realUri.endsWith(requestMethod.name())) { try { IActionResult result = (IActionResult) currMethod.invoke(currClass.newInstance(), req, resp); if (result != null && result.getData() != null) { resp.getWriter().print(result.getData()); resp.getWriter().flush(); } } catch (Exception e) { e.printStackTrace(); } break; } } break; } } } }
在Spring MVC中,我们可以通过配置找到对应的包下的Controller类,我们这里就不做这个了,将Controller跟BaseServlet置于同一个包目录下,在同一个包目录下进行查找,具体的实现上面注释也写的挺详细的,
就不在多解释了。
然后,我们来看看其中的一些小细节吧,我将返回的数据封装到一个接口内,IActionResult接口为:
public interface IActionResult { String getData(); }
我们Controller类都返回该接口的实现即可,类似我写了个公用的返回数据类;
public enum ErrorCodeResponse implements IActionResult { Success(0), Error(1), // 公共错误 10000+ ParamIsNull(10001, " %s 不能为空 "), AccountIsNull(10002, "用户账号不能为空"), PasswordIsNull(10003, "用户密码不能为空"); private int code; private String msg; private ErrorCodeResponse(int code) { this.code = code; } private ErrorCodeResponse(int code, String msg) { this.code = code; this.msg = msg; } public IActionResult getError() { return this; } public IActionResult getError(String param) { this.msg = String.format(this.msg, param); return this; } public IActionResult getError(int code, String param) { this.code = code; this.msg = param; return this; } public IActionResult getSuccess(String param) { this.msg = param; return this; } // 拓展参数value private Object data = null; public ErrorCodeResponse setExtraData(Object data) { this.data = data; return this; } @Override public String getData() { Gson gson = new Gson(); Map<String, Object> params = new HashMap<String, Object>(); params.put("code", this.code); params.put("msg", this.msg); if (Types.isNotEmpty(data)) { if (data instanceof String) { params.put("data", data); } else { params.put("data", gson.toJson(data)); } } else { params.put("data", ""); } return gson.toJson(params); } }
通过这个工具类,我们返回的结果会以JSON字符串进行返回;
上面还有个ClassUtil工具类,其实可以封装到BaseServlet里面,就像是Spring MVC那样,Controller的参数可以是一个JavaBean,但是我没去搞了,写这个也是闲的蛋疼而已。
public final class ClassUtil { /** * 将request请求数据转换为实体 * @param request * @param clazz * @return */ public static <T> T getObjectFromResquest(HttpServletRequest request, Class<T> clazz) { try { T t = clazz.newInstance(); Map<String, Method> methods = new HashMap<>(); for (Method method : clazz.getMethods()) { methods.put(method.getName(), method); } Enumeration<String> keys = request.getParameterNames(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); String value = request.getParameter(key); if (value.isEmpty()) continue; String fieldName = key.substring(0, 1).toUpperCase() + key.substring(1); Method setMethod = methods.get("set" + fieldName); if (setMethod == null) continue; invokeSetMethod(t, setMethod, value); } return t; } catch (InstantiationException | IllegalAccessException | SecurityException | IllegalArgumentException | InvocationTargetException e) { e.printStackTrace(); } return null; } private static void invokeSetMethod(Object obj, Method method, Object value) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { Class<?>[] types = method.getParameterTypes(); if (types != null && types.length == 1) { Class<?> typeClass = types[0]; if (value.getClass() == typeClass) { method.invoke(obj, value); return; } if (typeClass == String.class) method.invoke(obj, String.valueOf(value)); else if (typeClass == Integer.class || typeClass == int.class) { method.invoke(obj, Integer.valueOf(String.valueOf(value))); } else if (typeClass == Long.class || typeClass == long.class) { method.invoke(obj, Long.valueOf(String.valueOf(value))); } } } }
以上,就大概就是所有的流程,这样的话,我们就不用像使用HttpServlet那样,每个请求都新建一个Servlet了,通过注解,我们可以很方便的将每一个请求写好;
大概是闲的蛋疼吧,认真的态度随便的心情写的,多多指教,不喜勿喷。