【轉】SpringMVC源碼分析和一些經常使用最佳實踐

源地址 http://neoremind.com/2016/02/springmvc%E7%9A%84%E4%B8%80%E4%BA%9B%E5%B8%B8%E7%94%A8%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/前端

文章寫得很是好,轉過來學習下java

前言

本文分兩部分,第一部分剖析SpringMVC的源代碼,看看一個請求響應是如何處理,第二部分主要介紹一些使用中的最佳實踐,這些best practices有些比較common,有些比較tricky,旨在展現一個框架的活力以及一些能在平常項目中可以應用的技巧,這些技巧的線索均可以在第一部分的代碼剖析中找到,因此讀讀源代碼對於使用好任何框架都是很是有幫助的,正所謂「知其然,還要知其因此然」。web

另外,本文中所涉及的Spring版本是3.1.2RELEASE。算法

Part 1. SpringMVC請求響應模型

SpringMVC一直活躍在後端MVC框架的最前沿,不少web系統都是構建在此之上。最多見就是編寫一個Controller,代碼片斷以下:spring

@RequestMapping(value = "/getTemplateInfo", method = RequestMethod.GET)
@ResponseBody
public JsonObject<?> getTemplateInfo(@RequestParam(value = "userId", required = true) int userId, @RequestParam(value = "groupType") int groupType) {
 
    // ... logic here
 
}

以該例子爲背景,先簡單剖析下SpringMVC源代碼看看它的HTTP請求響應模型是怎樣的,跟着流程走一遍。後端

衆所周知,SpringMVC是創建在Servlet基礎之上,通常來講配置全部的請求都由DispatcherServlet來處理,從web.xml的配置中就能夠看出來。設計模式

<servlet>          
    <servlet-name>spring</servlet-name>          
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  </servlet>
<servlet-mapping>          
    <servlet-name>spring</servlet-name>          
    <url-pattern>/</url-pattern>  
</servlet-mapping>

DispatcherServlet是整個框架的核心所在,它既是一個標準的HttpServlet,也是利用開放封閉原則(Open-Closed)進行設計的經典框架。一句英文歸納就是「對擴展開發,對修改封閉」:數組

A key design principle in Spring Web MVC and in Spring in general is the "Open for extension,closed for modification" principle.服務器

在DispatcherServlet中能夠明顯看到:mvc

1)類中全部的變量聲明,幾乎都以接口的形式給出,並無綁定在具體的實現類上。

舉例來講,SpringMVC利用IoC動態初始化HandlerAdapter實例,也就說在applicationContext.xml中配置的一切接口實現的bean,若是名稱match,框架實際都默默的注入到了DispatcherServlet。

public static final String HANDLER_ADAPTER_BEAN_NAME = "handlerAdapter";
 
private List handlerAdapters;

2)使用模版方法模式,方便擴展。

所謂模板模式就是定義一個操做中的算法的骨架,而將一些步驟延遲到子類中。SpringMVC在保證整個框架流程穩定的狀況下,預留不少口子,而這些口子都是所謂的模板方法,能夠自由指定,從而保證了靈活性,接下來的不少使用最佳實踐都是基於這種設計模式才能夠實現。

例如,下面的代碼中doResolveException(..)就是一個口子,子類方法doResolveException(..)能夠定義具體如何處理異常。

public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
        Exception ex) {
    if (shouldApplyTo(request, handler)) {
        logException(ex, request);
        prepareResponse(ex, response);
        return doResolveException(request, response, handler, ex);
    } else {
        return null;
    }
}
 
protected abstract ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response,
        Object handler, Exception ex);

 
3)良好的抽象設計,是整個框架變得很是靈活。
舉例來講,在doDispatch(HttpServletRequest request, HttpServletResponse response)方法中有一段流程處理,大體能夠看出是獲取全部的攔截器,遍歷之,調用preHandle進行前置處理。

// Apply preHandle methods of registered interceptors.
HandlerInterceptor[] interceptors = mappedHandler.getInterceptors();
if (interceptors != null) {
    for (int i = 0; i < interceptors.length; i++) {
        HandlerInterceptor interceptor = interceptors[i];
        if (!interceptor.preHandle(processedRequest, response, mappedHandler.getHandler())) {
            triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, null);
            return;
        }
        interceptorIndex = i;
    }
}

而全部的攔截器都是HandlerInterceptor接口的實現,框架充分使用接口來回調這些開發人員指定的攔截器,這就是所謂的口子。

public interface HandlerInterceptor {
    boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}

帶着這些設計的思想,下面,真正進入請求響應處理的剖析。

整個流程能夠被大體描述爲:

一個http請求到達服務器,被DispatcherServlet接收。DispatcherServlet將請求委派給合適的處理器Controller,此時處理控制權到達Controller對象。Controller內部完成請求的數據模型的建立和業務邏輯的處理,而後再將填充了數據後的模型即model和控制權一併交還給DispatcherServlet,委派DispatcherServlet來渲染響應。DispatcherServlet再將這些數據和適當的數據模版視圖結合,向Response輸出響應。

這個流程以下圖所示:

圍繞DispatcherServlet的流程處理以下圖:

UML序列圖以下:

 

具體剖析DispatcherServlet,首先,客戶端發起請求,假如是一個GET請求,會由doGet(HttpServletRequest request, HttpServletResponse response)來處理,內部調用
void processRequest(HttpServletRequest request, HttpServletResponse response)。

在processRequest這一階段主要就是調用void doDispatch(HttpServletRequest request, HttpServletResponse response)方法。

在doDispatch主要有一下幾步操做。 

(1)調用DispatcherServlet#getHandler(HttpServletRequest request)方法返回一個HandlerExecutionChain對象。

內部實現以下:

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    for (HandlerMapping hm : this.handlerMappings) {
        if (logger.isTraceEnabled()) {
            logger.trace("Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName()
                    + "'");
        }
        HandlerExecutionChain handler = hm.getHandler(request);
        if (handler != null) {
            return handler;
        }
    }
    return null;
}

首先是遍歷初始化好的HandlerMapping,具體查看HandlerMapping實現類,例如框架提供的最經常使用的實現RequestMappingHandlerMapping,先看下HandlerMapping是如何初始化的,下面代碼從AbstractHandlerMethodMapping中摘取,描述了其過程:

// 在Spring容器中初始化
public void afterPropertiesSet() {
    initHandlerMethods();
}
 
// 初始化HandlerMethod
protected void initHandlerMethods() {
    String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ? BeanFactoryUtils
            .beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) : getApplicationContext()
            .getBeanNamesForType(Object.class));
 
    for (String beanName : beanNames) {
        if (isHandler(getApplicationContext().getType(beanName))) {
            detectHandlerMethods(beanName);
        }
    }
    handlerMethodsInitialized(getHandlerMethods());
}
 
// 看某個bean是不是controller
protected boolean isHandler(Class<?> beanType) {
    return ((AnnotationUtils.findAnnotation(beanType, Controller.class) != null) || (AnnotationUtils
            .findAnnotation(beanType, RequestMapping.class) != null));
}
 
// 獲取某個controller下全部的Method,作url path->Method的簡單關聯
protected void detectHandlerMethods(final Object handler) {
    Class<?> handlerType = (handler instanceof String) ? getApplicationContext().getType((String) handler)
            : handler.getClass();
 
    final Class<?> userType = ClassUtils.getUserClass(handlerType);
 
    Set methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() {
        public boolean matches(Method method) {
            return getMappingForMethod(method, userType) != null;
        }
    });
 
    for (Method method : methods) {
        T mapping = getMappingForMethod(method, userType);
        registerHandlerMethod(handler, method, mapping);
    }
}

HandlerMapping通常是在applicationContext.xml中定義的,在Spring啓動時候就會注入到DispatcherServlet中,它的初始化方式主要依賴於AbstractHandlerMethodMapping這個抽象類,利用initHandlerMethods(..)方法獲取全部Spring容器託管的bean,而後調用isHandler(..)看是不是@Controller註解修飾的bean,以後調用detectHandlerMethods(..)嘗試去解析bean中的方法,也就是去搜索@RequestMapping註解修飾的方法,將前端請求的url path,例如「/report/query」和具體的Method來作關聯映射,例如一個HandlerMapping內含的屬性以下,將前端的「/portal/ad/getTemplateInfo」與SiConfController.getTemplateInfo(int,int)方法相綁定。以下所示:

[/portal/ad/getTemplateInfo],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}=public com.baidu.beidou.ui.web.common.vo.JsonObject<?>com.baidu.beidou.ui.web.portal.ad.controller.SiConfController.getTemplateInfo(int,int)]

完成全部的搜索bean搜索後,調用registerHandlerMethod(..)將Method構造爲HandlerMethod,添加到HandlerMapping內含的屬性列表中:

private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>();

保存,這裏的T是泛型的,具體存放的最簡單就是url path信息。

繼續返回到DispatcherServlet#getHandler(HttpServletRequest request)方法中,遍歷上面講到的HandlerMapping,調用hm.getHandler(request)方法:

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    Object handler = getHandlerInternal(request);
    if (handler == null) {
        handler = getDefaultHandler();
    }
    if (handler == null) {
        return null;
    }
    // Bean name or resolved handler?
    if (handler instanceof String) {
        String handlerName = (String) handler;
        handler = getApplicationContext().getBean(handlerName);
    }
    return getHandlerExecutionChain(handler, request);
}
 
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
    HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain) ? (HandlerExecutionChain) handler
            : new HandlerExecutionChain(handler);
 
    chain.addInterceptors(getAdaptedInterceptors());
 
    String lookupPath = urlPathHelper.getLookupPathForRequest(request);
    for (MappedInterceptor mappedInterceptor : mappedInterceptors) {
        if (mappedInterceptor.matches(lookupPath, pathMatcher)) {
            chain.addInterceptor(mappedInterceptor.getInterceptor());
        }
    }
 
    return chain;
}

內部調用父類的AbstractHandlerMapping#getHandlerInternal(HttpServletRequest request),也就是說根據url path返回一個具體的HandlerMethod,而後調用getHandlerExecutionChain構形成一個HandlerExecutionChain,能夠看到就是在這個時候將全部根據xml的配置將攔截器添加到HandlerExecutionChain中的,這裏使用到了職責鏈模式。

 

(2)繼續回到主流程,調用DispatcherServlet#getHandlerAdapter(Object handler),代碼以下,

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
    for (HandlerAdapter ha : this.handlerAdapters) {
        if (logger.isTraceEnabled()) {
            logger.trace("Testing handler adapter [" + ha + "]");
        }
        if (ha.supports(handler)) {
            return ha;
        }
    }
    throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

這裏會遍歷全部配置在Spring容器已經配好的handlerAdapters,調用supports(..)方法,獲得第一個返回爲true的HandlerAdapter,這裏的參數實際上就是上面提到的就是HandlerMethod。 supports主要就是驗證某個HandlerMethod上定義的參數、返回值解析,是否能由該handlerAdapter處理。

HandlerAdapter最主要的方法就是處理http請求,在下面會更詳細的講解。

public interface HandlerAdapter {
    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}

 

(3) 開始進行攔截器處理,代碼以下:

// Apply preHandle methods of registered interceptors.
HandlerInterceptor[] interceptors = mappedHandler.getInterceptors();
if (interceptors != null) {
    for (int i = 0; i < interceptors.length; i++) {
        HandlerInterceptor interceptor = interceptors[i];
        if (!interceptor.preHandle(processedRequest, response, mappedHandler.getHandler())) {
            triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, null);
            return;
        }
        interceptorIndex = i;
    }
}

首先獲取全部的攔截器,而後依次遍歷調用HandlerExecutionChain#applyPreHandle(HttpServletRequest request, HttpServletResponse response)開始應用攔截器,一個一個調用其preHandle方法,若是有錯誤,直接退出調用afterCompletion方法,返回false。

 

(4)接着調用HandlerAdapter.handle(…)獲得一個ModelAndView對象,裏面封裝了數據對象及具體的View對象。

具體實現須要查看HandlerAdapter實現類。例如RequestMappingHandlerAdapter,如下代碼即從該類中截取。 

private ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response,
        HandlerMethod handlerMethod) throws Exception {
 
    ServletWebRequest webRequest = new ServletWebRequest(request, response);
 
    WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
    ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
    ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod, binderFactory);
 
    ModelAndViewContainer mavContainer = new ModelAndViewContainer();
    mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
    modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);
    mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
 
    requestMappingMethod.invokeAndHandle(webRequest, mavContainer);
    // ... return ModelAndView
}

首先,getDataBinderFactory(..)獲取全部指定Controller里加入的@InitBinder註解,自定義作數據轉換用,以後調用getModelFactory獲取一個最終生成model的工廠,而後構造ServletInvocableHandlerMethod方法,重點在於ServletInvocableHandlerMethod#invokeAndHandle(webRequest, mavContainer)方法,截取內部的實現以下: 

public final Object invokeForRequest(NativeWebRequest request, ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {
    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    Object returnValue = invoke(args);
    return returnValue;
}
 
private Object[] getMethodArgumentValues(NativeWebRequest request, ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {
 
    MethodParameter[] parameters = getMethodParameters();
    Object[] args = new Object[parameters.length];
    for (int i = 0; i < parameters.length; i++) {
        MethodParameter parameter = parameters[i];
        parameter.initParameterNameDiscovery(parameterNameDiscoverer);
        GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());
 
        args[i] = resolveProvidedArgument(parameter, providedArgs);
        if (args[i] != null) {
            continue;
        }
 
        if (argumentResolvers.supportsParameter(parameter)) {
            try {
                args[i] = argumentResolvers.resolveArgument(parameter, mavContainer, request, dataBinderFactory);
                continue;
            } catch (Exception ex) {
                if (logger.isTraceEnabled()) {
                    logger.trace(getArgumentResolutionErrorMessage("Error resolving argument", i), ex);
                }
                throw ex;
            }
        }
 
        if (args[i] == null) {
            String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i);
            throw new IllegalStateException(msg);
        }
    }
    return args;
}

依次遍歷目標調用方法上面的參數,嘗試從請求中解析參數與值而且作映射與bind,能夠看到這裏面argumentResolvers是核心,這個口子用於將前端請求與Controller上定義的參數類型相綁定,能夠天然想到這個抽象的設計,能夠給予框架使用者不少的靈活選擇。

 

(5)而後調用HandlerExecutionChain#applyPostHandle(…)再次應用攔截器,調用其postHandle方法。HandlerExecutionChain#applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv)

void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) throws Exception {
    if (getInterceptors() == null) {
        return;
    }
    for (int i = getInterceptors().length - 1; i >= 0; i--) {
        HandlerInterceptor interceptor = getInterceptors()[i];
        interceptor.postHandle(request, response, this.handler, mv);
    }
}

 

(6)最後一步processDispatchResult處理結果,相應給客戶端。

在processDispatchResult首先調用了render(mv, request, response)方法。

最後是HandlerExecutionChain#triggerAfterCompletion(..),調用攔截器的afterCompletion方法。攔截器處理流程至此結束。

至此一個請求響應過程結束。

 

 

Part 2. 一些最佳實踐

1. 使用WebDataBinder來作參數的個性化綁定

一般狀況下,框架能夠很好的處理前端傳遞k1=v2&k2=v2形式的POST data和GET請求參數,並將其轉換、映射爲Controller裏method的args[]類型,可是在某些狀況下,咱們有不少自定義的需求,例如對於字符串yyyyMMDD轉換爲Date對象,這時候自定義DataBinder就很是有用了,在上面源碼剖析的第(4)點介紹過。

一個更加trick的需求是,前端傳遞兩種cases的urls參數:

urls= http://c.admaster.com.cn/c/a25774,b200663567,c3353,i0,m101,h&urls=http://www.baidu.com urls= http://c.admaster.com.cn/c/a25774,b200663567,c3353,i0,m101,h

對於第一種,後端接收到的urls.size()=2,符合預期,而對於第二種,後端接收的urls.size()=5,不是預期的urls.size()=1,緣由就是SpringMVC進行參數映射綁定時,默認會自動把按照逗號分隔的參數映射成數組或者list的元素。對這個問題,一樣可使用WebDataBinder解決,解決代碼以下,只須要在Controller里加入一個@InitBinder修飾的方法,去在binder裏面加入自定義的參數解析方法便可。

@RequestMapping(value = "/getUrls", method = RequestMethod.GET)
@ResponseBody
public JsonObject<?> getUrls(@RequestParam(value = "urls") List urls) {
    JsonObject<?> result = JsonObject.create();
    System.out.println(urls);
    result.addData("urls", urls);
    return result;
}
 
@InitBinder
public void dataBinder(WebDataBinder binder) {
    PropertyEditor urlEditor = new PropertyEditorSupport() {
        @Override
        public void setValue(Object value) throws IllegalArgumentException {
            if (value instanceof List) {
                super.setValue(value);
            } else if (value.getClass().isArray() && value instanceof String[]) {
                super.setValue(Lists.newArrayList((String[]) value));
            }
        }
 
        @Override
        public void setAsText(String text) throws java.lang.IllegalArgumentException {
            if (text instanceof String) {
                setValue(Lists.newArrayList(text));
                return;
            }
            throw new IllegalArgumentException(text);
        }
    };
    binder.registerCustomEditor(List.class, urlEditor);
}

 

2. 使用高級的HandlerMethodArgumentResolver來實現參數的個性化解析

一般狀況下,對於參數key的解析、映射,框架會幫助咱們完成到對象的綁定,可是在某些遺留系統中,前端傳遞的參數與後端Form表單定義的命名不會相同,例如在某些系統中參數爲qp.page=1&qp.pageSize=50,然後端的Form表單類屬性命名不可能帶有點號,這時候咱們能夠自定義一個ArgumentResolver來本身設置參數對象。

例如,咱們的query方法簽名以下,QueryParamForm中的屬性名稱爲page、pageSize:

@RequestMapping("/dtList")
@ResponseBody
public JsonObject<genderviewitem> query(@Qp QueryParamForm form) {     ResultBundle<genderviewitem> res = reportService.queryGenderReport(toQP(form));     return toResponse(res, form); }</genderviewitem></genderviewitem>

Qp是一個註解:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Qp {
}

在handlerAdapter中自定義customArgumentResolvers:

<bean id="handlerAdapter"
class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <property name="customArgumentResolvers">
        <util:list>
            <ref bean="reportArgumentResolver" />
        </util:list>
    </property>
</bean>

ArgumentResolver的實現以下,只須要覆蓋兩個方法便可,在上面源碼剖析(4)中介紹過對於參數的解析介紹過。在這裏省略了QueryParamFormBuilder類,這個類主要就是去webRequest中主動取"qp.page"與"qp.pageSize"參數的值,利用反射去動態的set到一個空QueryParamForm對象的屬性中。 

@Component
public class ReportArgumentResolver implements HandlerMethodArgumentResolver {
 
    @Resource
    private QueryParamFormBuilder formBuilder;
 
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(Qp.class);
    }
 
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        if (parameter.getParameterType() == QueryParamForm.class) {
            return formBuilder.buildForm(webRequest);
        }
        return WebArgumentResolver.UNRESOLVED;
    }
 
}

 

3. 使用aspectj攔截器

以上面那個例子爲背景,若是要作全局的參數校驗,不必在每一個方法中主動寫方法,能夠利用AspectJ與Spring的集成,編入指定類到方法上的AOP切面,統一來作驗證。詳細代碼以下: 

@Component
@Aspect
public class ReportQueryParamInterceptor {
 
    private static final Logger LOG = LoggerFactory.getLogger(ReportQueryParamInterceptor.class);
 
    @Around("execution(* com.baidu.beidou.ui.web.portal.report.controller.*ReportController.query*(..))")
    public Object validate4Query(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature methodSignature = getMethodSignature(pjp);
        Object[] args = pjp.getArgs();
        if (args == null || args.length < 1 || !(args[0] instanceof QueryParamForm)) {
            LOG.warn("Request param is null or not instanceof QueryParamForm! " + args);
            throw new IllegalArgumentException("Request param error which should not happen!");
        }
 
        QueryParamForm form = (QueryParamForm) args[0];
        JsonObject response = (JsonObject) (methodSignature.getReturnType().newInstance());
 
        validateAndPrepareQueryParamForm(response, form);
        if (response.getStatus() != GlobalResponseStatusMsg.OK.getCode()) {
            return response;
        }
 
        return pjp.proceed();
    }
}

 

4. 全局錯誤處理,隱藏後端異常以及友好提示

一般狀況下,一個web系統,不該該像外部暴露過多的內部異常細節,那麼咱們能夠覆蓋掉SpringMVC提供的默認異常處理handler,定義本身的GlobalExceptionHandler,這裏面爲了覆蓋掉默認的handler,須要實現Ordered,而且賦值order爲Ordered.HIGHEST_PRECEDENCE。

在配置文件中使用本身的handler。

<bean id="exceptionHandler"
class="com.baidu.beidou.ui.web.common.handler.GlobalExceptionHandler">
</bean>

resolveException(..)方法內,能夠針對各類異常信息,去返回給前端不一樣的信息,包括錯誤返回碼等等。

public class GlobalExceptionHandler implements HandlerExceptionResolver, ApplicationContextAware, Ordered {
 
    protected ApplicationContext context;
 
    /**
     * 默認HandlerExceptionResolver優先級,設置爲最高,用於覆蓋系統默認的異常處理器
     */
    private int order = Ordered.HIGHEST_PRECEDENCE;
 
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) {
        ModelAndView model = new ModelAndView(new MappingJacksonJsonView());
        try {
            if (e instanceof TypeMismatchException) {
                LOG.warn("TypeMismatchException occurred. " + e.getMessage());
                return buildBizErrors((TypeMismatchException) e, model);
            } else if (e instanceof BindException) {
                LOG.warn("BindException occurred. " + e.getMessage());
                return buildBizErrors((BindException) e, model);
            } else if (e instanceof HttpRequestMethodNotSupportedException) {
                LOG.warn("HttpRequestMethodNotSupportedException occurred. " + e.getMessage());
                return buildError(model, GlobalResponseStatusMsg.REQUEST_HTTP_METHOD_ERROR);
            } else if (e instanceof MissingServletRequestParameterException) {
                LOG.warn("MissingServletRequestParameterException occurred. " + e.getMessage());
                return buildError(model, GlobalResponseStatusMsg.PARAM_MISS_ERROR);
            } else {
                LOG.error("System error occurred. " + e.getMessage(), e);
                return buildError(model, GlobalResponseStatusMsg.SYSTEM_ERROR);
            }
        } catch (Exception ex) {
            // Omit all detailed error message including stack trace to external user
            LOG.error("Unexpected error occurred! This should never happen! " + ex.getMessage(), ex);
            model.addObject("status", SYS_ERROR_CODE);
            model.addObject("msg", SYS_ERROR_MSG);
            return model;
        }
    }
}

 

5. Spring自帶攔截器

攔截器最多見的使用場景就是日誌、登錄、權限驗證等。下面以權限驗證爲例,通常狀況下,登錄的用戶會有不一樣的訪問權限,對於controller裏定義的方法進行有限制的調用,爲了更好的解耦,能夠定義一個公共的攔截器。 

public class PriviledgeInterceptor implements HandlerInterceptor {
 
    @Override
    boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 獲取線程上下文的visitor
        Visitor visitor = ThreadContext.getSessionVisitor();
        Preconditions.checkNotNull(visitor, "Visitor should NOT be null in ThreadContext!");
 
        // 獲取權限集合
        Set authSet = visitor.getAuths();
        if (CollectionUtils.isEmpty(authSet)) {
            LOG.error("Visitor does NOT get any auths, userid=" + visitor.getUserid());
            returnJsonSystemError(request, response, GlobalResponseStatusMsg.AUTH_DENIED);
            return false;
        }
 
        // 結合controller裏定義方法的註解來作驗證
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Privilege privilege = handlerMethod.getMethodAnnotation(Privilege.class);
        if (privilege != null) {
            if (authSet.contains(privilege.value())) {
                return true;
            }
            LOG.error("Visitor does NOT have auth={} on controller={}, userid={}", new Object[] { privilege.value(),
                    getBeanTypeAndMethodName(handlerMethod), visitor.getUserid() });
            returnJsonSystemError(request, response, GlobalResponseStatusMsg.AUTH_DENIED);
            return false;
        }
 
    }
}

 controller定義以下:

@Controller
@RequestMapping("/test")
@Privilege(PriviledgeConstant.BEIDOU_CPROUNIT)
public class SiConfController {
}

 

6. 使用模板方法來簡化代碼開發

對於不少的類似邏輯,能夠利用模板模式,把公共的操做封裝到父類controller中。例如對於一個下載報表的需求,能夠隱藏具體的寫流等底層操做,將這些模板抽象化到父類BaseController中,子類只須要去實現傳入一個調用獲取報表數據Callback來,這和Hibernate的callback思想殊途同歸。

@RequestMapping(value = "/downloadDtList")
@ResponseBody
public HttpEntity<byte[]> download(@RequestParam(value = PortalReportConstants.DOWNLOAD_POST_PARAM, required = true) String iframePostParams) {
    return toHttpEntity(new ReportCallback<ResultBundle<?>>() {
        public ResultBundle<GenderViewItem> call(QueryParamForm form) {
            return reportService.queryGenderReport(toQP(form));
        }
    });
}

 

總結

上面的記錄是在2014年春作報表系統重構出web-ui模塊的一些最佳實踐,可做爲一個系統中的portal,可前端js或者API客戶端打交道的公共複用模塊。深刻到SpringMVC的源代碼才感覺的到其強大之處,但願你與我共勉,知其然還要知其因此然。

相關文章
相關標籤/搜索