手动写个类似的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了,通过注解,我们可以很方便的将每一个请求写好;

大概是闲的蛋疼吧,认真的态度随便的心情写的,多多指教,不喜勿喷。

 

posted @ 2018-09-12 11:58  JhonMr  阅读(642)  评论(0编辑  收藏  举报