文档章节

【转】SpringMVC源码分析和一些常用最佳实践

王小明123
 王小明123
发布于 2017/08/08 20:14
字数 4327
阅读 75
收藏 1
点赞 0
评论 0

源地址 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/

文章写得非常好,转过来学习下

前言

本文分两部分,第一部分剖析SpringMVC的源代码,看看一个请求响应是如何处理,第二部分主要介绍一些使用中的最佳实践,这些best practices有些比较common,有些比较tricky,旨在展示一个框架的活力以及一些能在日常项目中能够应用的技巧,这些技巧的线索都可以在第一部分的代码剖析中找到,所以读读源代码对于使用好任何框架都是非常有帮助的,正所谓“知其然,还要知其所以然”。

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

Part 1. SpringMVC请求响应模型

SpringMVC一直活跃在后端MVC框架的最前沿,很多web系统都是构建在此之上。最常见就是编写一个Controller,代码片段如下:

@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中可以明显看到:

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的源代码才感受的到其强大之处,希望你与我共勉,知其然还要知其所以然。

© 著作权归作者所有

共有 人打赏支持
王小明123
粉丝 142
博文 262
码字总数 297792
作品 0
杭州
程序员
三流程序员与一流程序员之间的区别,看看你是属于哪一类?

源码系列 手写spring mvc框架 基于Spring JDBC手写ORM框架 实现自己的MyBatis Spring AOP实战之源码分析 Spring IOC高级特性应用分析 ORM框架底层实现原理剖析 手写Spring MVC框架实现 手把手...

茶轴的青春 ⋅ 04/17 ⋅ 0

Spring IOC 容器源码分析系列文章导读

1. 简介 Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本。经过十几年的迭代,现在的 Spring 框架已经非常成熟了。Spring 包含了众多模块,包括但不限于...

coolblog ⋅ 05/30 ⋅ 0

Java开发者不会这些永远都只能是三流程序员,细数一下你是不是?

源码系列 手写spring mvc框架 基于Spring JDBC手写ORM框架 实现自己的MyBatis Spring AOP实战之源码分析 Spring IOC高级特性应用分析 ORM框架底层实现原理剖析 手写Spring MVC框架实现 手把手...

美的让人心动 ⋅ 04/16 ⋅ 0

Spring集成Redis方案(spring-data-redis)(基于Jedis的单机模式)(待实践)

说明:请注意Spring Data Redis的版本以及Spring的版本!最新版本的Spring Data Redis已经去除Jedis的依赖包,需要自行引入,这个是个坑点。并且会与一些低版本的Spring有冲突,要看官方文档...

easonjim ⋅ 2017/10/05 ⋅ 0

【小马哥】Spring Cloud系列讲座

这里为大家推荐一个不错的Spring Cloud系列讲座,讲师介绍如下: 小马哥,阿里巴巴技术专家,从事十余年Java EE 开发,国内微服务技术讲师。目前主要负责微服务技术推广、架构设计、基础设施...

杜琪 ⋅ 03/02 ⋅ 0

一文读懂Spring Boot、微服务架构和大数据治理之间的故事

微服务架构 微服务的诞生并非偶然,它是在互联网高速发展,技术日新月异的变化以及传统架构无法适应快速变化等多重因素的推动下诞生的产物。互联网时代的产品通常有两类特点:需求变化快和用...

纯洁微笑 ⋅ 05/10 ⋅ 0

【spring boot 系列】spring security 实践 + 源码分析

前言 本文将从示例、原理、应用3个方面介绍 spring data jpa。 以下分析基于spring boot 2.0 + spring 5.0.4版本源码 概述 Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明...

java高级架构牛人 ⋅ 06/06 ⋅ 0

阿里,百度,腾讯等一线互联网公司中,Java开发的招聘标准

金三银四的跳槽热潮即将过去,在这两个月的跳槽的旺季中,作为互联网行业的三大巨头,百度、阿里巴巴、腾讯对于互联网人才有很大的吸引力,他们的员工也是众多互联网同行觊觎的资深工程师、管...

javaxuexi123 ⋅ 04/20 ⋅ 0

微服务架构 spring boot 那些最基础的知识点

一、创建SpringBoot项目 概念 Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再...

烂猪皮 ⋅ 05/09 ⋅ 0

Spring Boot 启动过程分析

1. Spring Boot 入口——main方法 从上面代码可以看出,Annotation定义(@SpringBootApplication)和类定义(SpringApplication.run)最为耀眼,所以分析 Spring Boot 启动过程,我们就从这两...

徐志毅 ⋅ 05/27 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

聊聊spring cloud gateway的LoadBalancerClientFilter

序 本文主要研究一下spring cloud gateway的LoadBalancerClientFilter GatewayLoadBalancerClientAutoConfiguration spring-cloud-gateway-core-2.0.0.RELEASE-sources.jar!/org/springfram......

go4it ⋅ 30分钟前 ⋅ 0

详解:Nginx反代实现Kibana登录认证功能

Kibana 5.5 版后,已不支持认证功能,也就是说,直接打开页面就能管理,想想都不安全,不过官方提供了 X-Pack 认证,但有时间限制。毕竟X-Pack是商业版。 下面我将操作如何使用Nginx反向代理...

问题终结者 ⋅ 37分钟前 ⋅ 0

002、nginx配置虚拟主机

一、nginx配置虚拟主机可分为三种方式,分别为: 1、基于域名的虚拟主机,通过域名来区分虚拟主机——应用:外部网站 2、基于端口的虚拟主机,通过端口来区分虚拟主机——应用:公司内部网站...

北岩 ⋅ 40分钟前 ⋅ 0

shell脚本之死循环写法

最近在学习写shell脚本,在练习if while等流程控制时,突然它们的死循环写法是怎么样的?经过百度与亲测记录如下: for死循环 #! /bin/bashfor ((;;));do date sleep 1d...

hensemlee ⋅ 42分钟前 ⋅ 0

苹果的ARKit2.0有多可怕,看了就知道

序言 ARKit主要由三部分组成: 跟踪(Tracking) 跟踪是ARKit的核心组件之一,其提供了设备在物理世界中的位置与方向信息,并对物体进行跟踪,如人脸。 2.场景理解(Scene Understanding) 场...

_小迷糊 ⋅ 43分钟前 ⋅ 0

5.1 vim介绍 5.2 vim移动光标 5.3 ,5.4vim一般模式下移动光标,复制粘贴

vim命令 vim是vi的一个升级版;vim可以显示文字的颜色 安装vim这一个包vim-enhanced 如果不知道安装包,可以使用 命令下面命令来查看vim命令是那个包安装的。 [root@linux-128 ~]# yum prov...

Linux_老吴 ⋅ 46分钟前 ⋅ 0

vim一般模式

vim 是什么 vim是什么 ? 在之前接触Linux,编辑网卡配置文件的时候我们用过了vi ,vim简单说就是vi的升级版,它跟vi一样是Linux系统中的一个文本编辑工具。 如果系统中没有vim ,需要安装一...

李超小牛子 ⋅ 54分钟前 ⋅ 0

docker实战

构建企业级Docker虚拟化平台实战 重点剖析虚拟化和云计算概念; 分析Docker虚拟化的概念和原理; 从0开始实战Docker虚拟化平台; 基于Docker构建Nginx WEB服务器和CentOS虚拟机; 基于开源监...

寰宇01 ⋅ 今天 ⋅ 0

vim介绍、vim颜色显示和移动光标、vim一般模式下移动光标、一般模式下复制粘贴剪切

VIM Vim 是 UNIX 文本编辑器 Vi 的加强版本,加入了更多特性来帮助编辑源代码。Vim 的部分增强功能包括文件比较(vimdiff),语法高亮,全面的帮助系统,本地脚本(Vimscript),和便于选择的...

蛋黄Yolks ⋅ 今天 ⋅ 0

springboot+mockito测试controller层遇到的问题

使用MockitoJUnitRunner测试的一个例子,原来报错无法找到bean, 类似的异常如下:createBeanError..... 原因:是因为@Runwith使用了SpringRunner,应该修改为MockitoJUnitRunner 代码如下: ...

writeademo ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部