從零開始實現一個簡易的Java MVC框架(九)--優化MVC代碼

前言

從零開始實現一個簡易的Java MVC框架(七)--實現MVC中實現了doodle框架的MVC的功能,不過最後指出代碼的邏輯不是很好,在這一章節就將這一部分代碼進行優化。java

優化的目標是1.去除DispatcherServlet請求分發器中的http邏輯代碼;2.將ControllerHandlerResultRender中代碼按功能細分出來,使其各司其職。git

修改DispatcherServlet

建立接口

先在com.zbw.mvc包下建立兩個包handler和render,分別用於放ControllerHandlerResultRender拆分出來的功能類。github

再在這兩個包下建立兩個接口,以便後面的功能類都按這個接口規範。web

package com.zbw.mvc.handler;

import com.zbw.mvc.RequestHandlerChain;

/**
 * 請求執行器 Handler
 */
public interface Handler {
    /**
     * 請求的執行器
     */
    boolean handle(final RequestHandlerChain handlerChain) throws Exception;
}
package com.zbw.mvc.render;

import com.zbw.mvc.RequestHandlerChain;

/**
 * 渲染請求結果 interface
 */
public interface Render {
    /**
     * 執行渲染
     */
    void render(RequestHandlerChain handlerChain) throws Exception;
}

實現RequestHandlerChain

上面兩個接口都有個參數RequestHandlerChain,這個類是整個請求的執行鏈,用於存儲整個請求須要保存的一些屬性和串聯整個請求。spring

在com.zbw.mvc下建立這個類json

package com.zbw.mvc;
import ...

/**
 * http請求處理鏈
 */
@Data
@Slf4j
public class RequestHandlerChain {
    /**
     * Handler迭代器
     */
    private Iterator<Handler> handlerIt;

    /**
     * 請求request
     */
    private HttpServletRequest request;

    /**
     * 請求response
     */
    private HttpServletResponse response;

    /**
     * 請求http方法
     */
    private String requestMethod;

    /**
     * 請求http路徑
     */
    private String requestPath;

    /**
     * 請求狀態碼
     */
    private int responseStatus;

    /**
     * 請求結果處理器
     */
    private Render render;

    public RequestHandlerChain(Iterator<Handler> handlerIt, HttpServletRequest request, HttpServletResponse response) {
        this.handlerIt = handlerIt;
        this.request = request;
        this.response = response;
        this.requestMethod = request.getMethod();
        this.requestPath = request.getPathInfo();
        this.responseStatus = HttpServletResponse.SC_OK;
    }

    /**
     * 執行請求鏈
     */
    public void doHandlerChain() {
        try {
            while (handlerIt.hasNext()) {
                if (!handlerIt.next().handle(this)) {
                    break;
                }
            }
        } catch (Exception e) {
            log.error("doHandlerChain error", e);
            render = new InternalErrorRender();
        }
    }

    /**
     * 執行處理器
     */
    public void doRender() {
        if (null == render) {
            render = new DefaultRender();
        }
        try {
            render.render(this);
        } catch (Exception e) {
            log.error("doRender", e);
            throw new RuntimeException(e);
        }
    }
}

在這個類中除了存儲http請求信息之外,還有Handler迭代器handlerIt和請求結果處理器Rendertomcat

doHandlerChain()方法就會迭代執行handlerIt中的Handler的handle()方法,而且會根據每一個Handler返回的值來判斷是否繼續往下執行下一個Handler。服務器

doRender()方法用於調用Render中的render()方法。mvc

更改DispatcherServlet

接下來就能夠修改DispatcherServlet請求轉發器了。app

package com.zbw.mvc;
import ...

/**
 * DispatcherServlet 全部http請求都由此Servlet轉發
 */
@Slf4j
public class DispatcherServlet extends HttpServlet {

    /**
     * 請求執行鏈
     */
    private final List<Handler> HANDLER = new ArrayList<>();

    /**
     * 執行請求
     */
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        RequestHandlerChain handlerChain = new RequestHandlerChain(HANDLER.iterator(), req, resp);
        handlerChain.doHandlerChain();
        handlerChain.doRender();
    }
}

能夠看到如今DispatcherServlet已經很簡潔了,把請求的邏輯代碼交給RequestHandlerChain處理,本身沒有多餘的http邏輯代碼。

實現幾種Handler

上面只建立了Handler的接口沒有實現類,如今就實現幾個Handler的實現類。這些實現類只實現了簡單的一些http請求的功能,你們能夠本身根據狀況開發更多的實現類。

PreRequestHandler

首先是PreRequestHandler,用於預處理http的一些信息,好比設置http編碼,處理請求url,打印一些信息等。

package com.zbw.mvc.handler;
import ...

/**
 * 請求預處理
 */
@Slf4j
public class PreRequestHandler implements Handler {
    @Override
    public boolean handle(final RequestHandlerChain handlerChain) throws Exception {
        // 設置請求編碼方式
        handlerChain.getRequest().setCharacterEncoding("UTF-8");
        String requestPath = handlerChain.getRequestPath();
        if (requestPath.length() > 1 && requestPath.endsWith("/")) {
            handlerChain.setRequestPath(requestPath.substring(0, requestPath.length() - 1));
        }
        log.info("[Doodle] {} {}", handlerChain.getRequestMethod(), handlerChain.getRequestPath());
        return true;
    }
}

SimpleUrlHandler

接下來是SimpleUrlHandler,用於處理靜態資源,當碰到資源是靜態資源時就直接轉發請求到Tomcat默認的servlet去。

package com.zbw.mvc.handler;
import ...

/**
 * 普通url請求執行
 * 主要處理靜態資源
 */
@Slf4j
public class SimpleUrlHandler implements Handler {
    /**
     * tomcat默認RequestDispatcher的名稱
     * TODO: 其餘服務器默認的RequestDispatcher.如WebLogic爲FileServlet
     */
    private static final String TOMCAT_DEFAULT_SERVLET = "default";

    /**
     * 默認的RequestDispatcher,處理靜態資源
     */
    private RequestDispatcher defaultServlet;

    public SimpleUrlHandler(ServletContext servletContext) {
        defaultServlet = servletContext.getNamedDispatcher(TOMCAT_DEFAULT_SERVLET);

        if (null == defaultServlet) {
            throw new RuntimeException("沒有默認的Servlet");
        }

        log.info("The default servlet for serving static resource is [{}]", TOMCAT_DEFAULT_SERVLET);
    }


    @Override
    public boolean handle(final RequestHandlerChain handlerChain) throws Exception {
        if (isStaticResource(handlerChain.getRequestPath())) {
            defaultServlet.forward(handlerChain.getRequest(), handlerChain.getResponse());
            return false;
        }
        return true;
    }

    /**
     * 是否爲靜態資源
     */
    private boolean isStaticResource(String url) {
        return url.startsWith(Doodle.getConfiguration().getAssetPath());
    }
}

JspHandler

而後是處理jsp頁面的實現類JspHandler,當碰到資源是jsp頁面時就直接轉發請求到Tomcat的jsp的servlet去。

package com.zbw.mvc.handler;
import ...

/**
 * jsp請求處理
 * 主要負責jsp資源請求
 */
public class JspHandler implements Handler {
    /**
     * jsp請求的RequestDispatcher的名稱
     */
    private static final String JSP_SERVLET = "jsp";

    /**
     * jsp的RequestDispatcher,處理jsp資源
     */
    private RequestDispatcher jspServlet;

    public JspHandler(ServletContext servletContext) {
        jspServlet = servletContext.getNamedDispatcher(JSP_SERVLET);
        if (null == jspServlet) {
            throw new RuntimeException("沒有jsp Servlet");
        }
    }

    @Override
    public boolean handle(final RequestHandlerChain handlerChain) throws Exception {
        if (isPageView(handlerChain.getRequestPath())) {
            jspServlet.forward(handlerChain.getRequest(), handlerChain.getResponse());
            return false;
        }
        return true;
    }

    /**
     * 是否爲jsp資源
     */
    private boolean isPageView(String url) {
        return url.startsWith(Doodle.getConfiguration().getViewPath());
    }
}

ControllerHandler

最後就是ControllerHandler,這個和從零開始實現一個簡易的Java MVC框架(七)--實現MVC中的ControllerHandler功能同樣,用於處理請求中數據和controller對應的關係。

package com.zbw.mvc.handler;
import ...

/**
 * Controller請求處理
 */
@Slf4j
public class ControllerHandler implements Handler {
    /**
     * 請求信息和controller信息關係map
     */
    private Map<PathInfo, ControllerInfo> pathControllerMap = new ConcurrentHashMap<>();
    /**
     * bean容器
     */
    private BeanContainer beanContainer;

    public ControllerHandler() {
        beanContainer = BeanContainer.getInstance();

        Set<Class<?>> mappingSet = beanContainer.getClassesByAnnotation(RequestMapping.class);
        this.initPathControllerMap(mappingSet);
    }

    @Override
    public boolean handle(final RequestHandlerChain handlerChain) throws Exception {
        String method = handlerChain.getRequestMethod();
        String path = handlerChain.getRequestPath();
        ControllerInfo controllerInfo = pathControllerMap.get(new PathInfo(method, path));
        if (null == controllerInfo) {
            handlerChain.setRender(new NotFoundRender());
            return false;
        }
        Object result = invokeController(controllerInfo, handlerChain.getRequest());
        setRender(result, controllerInfo, handlerChain);
        return true;
    }

    /**
     * 執行controller方法
     */
    private Object invokeController(ControllerInfo controllerInfo, HttpServletRequest request) {
        Map<String, String> requestParams = getRequestParams(request);
        List<Object> methodParams = instantiateMethodArgs(controllerInfo.getMethodParameter(), requestParams);

        Object controller = beanContainer.getBean(controllerInfo.getControllerClass());
        Method invokeMethod = controllerInfo.getInvokeMethod();
        invokeMethod.setAccessible(true);
        Object result;
        try {
            if (methodParams.size() == 0) {
                result = invokeMethod.invoke(controller);
            } else {
                result = invokeMethod.invoke(controller, methodParams.toArray());
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return result;
    }

    /**
     * 設置請求結果執行器
     */
    private void setRender(Object result, ControllerInfo controllerInfo, RequestHandlerChain handlerChain) {
        if (null == result) {
            return;
        }
        Render render;
        boolean isJson = controllerInfo.getInvokeMethod().isAnnotationPresent(ResponseBody.class);
        if (isJson) {
            render = new JsonRender(result);
        } else {
            render = new ViewRender(result);
        }
        handlerChain.setRender(render);
    }

    /**
     * 初始化pathControllerMap
     */
    private void initPathControllerMap(Set<Class<?>> mappingSet) {
        mappingSet.forEach(this::addPathController);
    }

    /**
     * 添加controllerInfo到pathControllerMap中
     */
    private void addPathController(Class<?> clz) {
        RequestMapping requestMapping = clz.getAnnotation(RequestMapping.class);
        String basePath = requestMapping.value();
        if (!basePath.startsWith("/")) {
            basePath = "/" + basePath;
        }
        for (Method method : clz.getDeclaredMethods()) {
            if (method.isAnnotationPresent(RequestMapping.class)) {
                RequestMapping methodRequest = method.getAnnotation(RequestMapping.class);
                String methodPath = methodRequest.value();
                if (!methodPath.startsWith("/")) {
                    methodPath = "/" + methodPath;
                }
                String url = basePath + methodPath;
                Map<String, Class<?>> methodParams = this.getMethodParams(method);
                String httpMethod = String.valueOf(methodRequest.method());
                PathInfo pathInfo = new PathInfo(httpMethod, url);
                if (pathControllerMap.containsKey(pathInfo)) {
                    log.warn("url:{} 重複註冊", pathInfo.getHttpPath());
                }
                ControllerInfo controllerInfo = new ControllerInfo(clz, method, methodParams);
                this.pathControllerMap.put(pathInfo, controllerInfo);
                log.info("mapped:[{},method=[{}]] controller:[{}@{}]",
                        pathInfo.getHttpPath(), pathInfo.getHttpMethod(),
                        controllerInfo.getControllerClass().getName(), controllerInfo.getInvokeMethod().getName());
            }
        }
    }

    /**
     * 獲取執行方法的參數
     */
    private Map<String, Class<?>> getMethodParams(Method method) {
        Map<String, Class<?>> map = new HashMap<>();
        for (Parameter parameter : method.getParameters()) {
            RequestParam param = parameter.getAnnotation(RequestParam.class);
            // TODO: 不使用註解匹配參數名字
            if (null == param) {
                throw new RuntimeException("必須有RequestParam指定的參數名");
            }
            map.put(param.value(), parameter.getType());
        }
        return map;
    }

    /**
     * 獲取HttpServletRequest中的參數
     */
    private Map<String, String> getRequestParams(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        //GET和POST方法是這樣獲取請求參數的
        request.getParameterMap().forEach((paramName, paramsValues) -> {
            if (ValidateUtil.isNotEmpty(paramsValues)) {
                paramMap.put(paramName, paramsValues[0]);
            }
        });
        // TODO: Body、Path、Header等方式的請求參數獲取
        return paramMap;
    }

    /**
     * 實例化方法參數
     */
    private List<Object> instantiateMethodArgs(Map<String, Class<?>> methodParams, Map<String, String> requestParams) {
        return methodParams.keySet().stream().map(paramName -> {
            Class<?> type = methodParams.get(paramName);
            String requestValue = requestParams.get(paramName);
            Object value;
            if (null == requestValue) {
                value = CastUtil.primitiveNull(type);
            } else {
                value = CastUtil.convert(type, requestValue);
                // TODO: 實現非原生類的參數實例化
            }
            return value;
        }).collect(Collectors.toList());
    }

}

初始化HANDLER列表和去除TomcatServer的多餘代碼

剛纔實現的幾個HANDLER還須要初始化,就在DispatcherServletinit()方法中初始化。注意初始化的順序會決定其在RequestHandlerChain執行鏈中執行的前後。

...
@Slf4j
public class DispatcherServlet extends HttpServlet {
    ...

    /**
     * 初始化Servlet
     */
    @Override
    public void init() throws ServletException {
        HANDLER.add(new PreRequestHandler());
        HANDLER.add(new SimpleUrlHandler(getServletContext()));
        HANDLER.add(new JspHandler(getServletContext()));
        HANDLER.add(new ControllerHandler());
    }
    ...
}

而後去除TomcatServerJspServletDefaultServlet兩個servlet的初始化,由於已經在 JspHandlerSimpleUrlHandler中初始化了這兩個servlet。

...
@Slf4j
public class TomcatServer implements Server {
    ...
    public TomcatServer(Configuration configuration) {
        try {
            this.tomcat = new Tomcat();
            tomcat.setBaseDir(configuration.getDocBase());
            tomcat.setPort(configuration.getServerPort());

            File root = getRootFolder();
            File webContentFolder = new File(root.getAbsolutePath(), configuration.getResourcePath());
            if (!webContentFolder.exists()) {
                webContentFolder = Files.createTempDirectory("default-doc-base").toFile();
            }

            log.info("Tomcat:configuring app with basedir: [{}]", webContentFolder.getAbsolutePath());
            StandardContext ctx = (StandardContext) tomcat.addWebapp(configuration.getContextPath(), webContentFolder.getAbsolutePath());
            ctx.setParentClassLoader(this.getClass().getClassLoader());

            WebResourceRoot resources = new StandardRoot(ctx);
            ctx.setResources(resources);
            
            // 去除了JspHandler和SimpleUrlHandler這兩個servlet的註冊
            
            tomcat.addServlet(configuration.getContextPath(), "dispatcherServlet", new DispatcherServlet()).setLoadOnStartup(0);
            ctx.addServletMappingDecoded("/*", "dispatcherServlet");
        } catch (Exception e) {
            log.error("初始化Tomcat失敗", e);
            throw new RuntimeException(e);
        }
    }
}

實現幾種Render

上面建立的Render接口也須要一些實現類。一樣的,這些Render也只是實現基本的功能 ,你們能夠本身根據狀況開發更多。

DefaultRender

這個是默認的Render,設置HttpServletResponse中的status爲RequestHandlerChain中StatusCode。

package com.zbw.mvc.render;
import ...
/**
 * 默認渲染 200
 */
public class DefaultRender implements Render {
    @Override
    public void render(RequestHandlerChain handlerChain) throws Exception {
        int status = handlerChain.getResponseStatus();
        handlerChain.getResponse().setStatus(status);
    }
}

InternalErrorRender

這個Render返回StatusCode爲500

package com.zbw.mvc.render;
import ...
/**
 * 渲染500
 */
public class InternalErrorRender implements Render {
    @Override
    public void render(RequestHandlerChain handlerChain) throws Exception {
        handlerChain.getResponse().sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    }
}

NotFoundRender

這個Render返回StatusCode爲404

package com.zbw.mvc.render;
import ...

/**
 * 渲染404
 */
public class NotFoundRender implements Render {
    @Override
    public void render(RequestHandlerChain handlerChain) throws Exception {
        handlerChain.getResponse().sendError(HttpServletResponse.SC_NOT_FOUND);
    }
}

JsonRender

這個Render返回json數據,當Handler請求發現返回數據爲json格式時,就用這個Render

package com.zbw.mvc.render;
import ...

/**
 * 渲染json
 */
@Slf4j
public class JsonRender implements Render {
    private Object jsonData;
    public JsonRender(Object jsonData) {
        this.jsonData = jsonData;
    }
    @Override
    public void render(RequestHandlerChain handlerChain) throws Exception {
        // 設置響應頭
        handlerChain.getResponse().setContentType("application/json");
        handlerChain.getResponse().setCharacterEncoding("UTF-8");
        // 向響應中寫入數據
        try (PrintWriter writer = handlerChain.getResponse().getWriter()) {
            writer.write(JSON.toJSONString(jsonData));
            writer.flush();
        }
    }
}

ViewRender

這個Render跳轉到頁面,將ModelAndView中的信息存到HttpServletRequest中並跳轉到對應頁面

package com.zbw.mvc.render;
import ...

/**
 * 渲染頁面
 */
@Slf4j
public class ViewRender implements Render {
    private ModelAndView mv;
    public ViewRender(Object mv) {
        if (mv instanceof ModelAndView) {
            this.mv = (ModelAndView) mv;
        } else if (mv instanceof String) {
            this.mv = new ModelAndView().setView((String) mv);
        } else {
            throw new RuntimeException("返回類型不合法");
        }
    }
    @Override
    public void render(RequestHandlerChain handlerChain) throws Exception {
        HttpServletRequest req = handlerChain.getRequest();
        HttpServletResponse resp = handlerChain.getResponse();
        String path = mv.getView();
        Map<String, Object> model = mv.getModel();
        model.forEach(req::setAttribute);
        req.getRequestDispatcher(Doodle.getConfiguration().getViewPath() + path).forward(req, resp);
    }
}

結語

至此,MVC的優化完成了,同時整個doodle框架的代碼也算是完成了。

雖然doodle早已完成,可是講解的文章託託延延到如今才完成。

在剛完成doodle時感受整個框架已經成型了,可是在寫這個系列文章的過程當中才真正發現欠缺的還有很是很是多,甚至以爲把它稱爲框架都有些擡舉它了呢。

只能說在實現它而後再寫這個系列的文章以後對spring的崇拜之心更加深了,其被javaer普遍使用和拜讀果真是有緣由的。

另外也感謝你們閱讀這個系列的文章,若是對你們有所幫助的話能夠去給個人項目加個star,有什麼問題和建議也能夠提出來交流交流。

這個系列的全部文章我都放在個人博客上了:http://zzzzbw.cn/


源碼地址:doodle

原文地址:從零開始實現一個簡易的Java MVC框架(九)--優化MVC代碼

相關文章
相關標籤/搜索