SpringMVC源码分析
来源:互联网 发布:vm mac 编辑:程序博客网 时间:2024/06/10 11:13
今天七夕的,何谓七夕,何谓情人节,我能不能与我的织女相遇呢 ?不扯淡了,直接切入正题,你好,我好,不如大家好,所谓大家好才是真的好。
SpringMVC有三个层次,分别是:HttpServletBean 和 FrameworkServlet、DispatcherServlet
SpringMVC 实际上市基于方法(handler:Method)处理模式 所以它可以使用单例
Struts:实际上是基于类(class)处理模式
HttpServletBean 继承 JavaServlet当中的HttpServlet,其中的作用将servlet当中的参数把它设置到相应的属性,传递给spring的框架,springmvc上下文和spring跟环境是一个继承关系,所以通过我们的web容器实现servlet规范来执行这些spring的初始化,服务,销毁这一套
<!-- 启用spring mvc 注解 --><context:annotation-config /><mvc:annotation-driven conversion-service="conversionService"/><bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <set> <bean class="com.zhijin.converter.TimestampConverter"></bean> </set> </property></bean><context:component-scan base-package="com.zhijin" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/></context:component-scan><mvc:default-servlet-handler />
本文分两部分,第一部分剖析SpringMVC的源代码,看看一个请求响应是如何处理,第二部分主要介绍一些使用中的最佳实践,这些best practices有些比较common,有些比较tricky,旨在展示一个框架的活力以及一些能在日常项目中能够应用的技巧,这些技巧的线索都可以在第一部分的代码剖析中找到,所以读读源代码对于使用好任何框架都是非常有帮助的,正所谓“知其然,还要知其所以然”。
另外,本文中所涉及的Spring版本是3.1.2RELEASE。
Part 1. SpringMVC请求响应模型
SpringMVC一直活跃在后端MVC框架的最前沿,很多web系统都是构建在此之上。最常见就是编写一个Controller,代码片段如下:
@RequestMapping(value = "/getTemplateInfo", method = RequestMethod.GET)@ResponseBodypublic 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();}// 初始化HandlerMethodprotected 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是否是controllerprotected 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)@ResponseBodypublic JsonObject<?> getUrls(@RequestParam(value = "urls") List urls) { JsonObject<?> result = JsonObject.create(); System.out.println(urls); result.addData("urls", urls); return result;}@InitBinderpublic 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);}
- 使用高级的HandlerMethodArgumentResolver来实现参数的个性化解析
通常情况下,对于参数key的解析、映射,框架会帮助我们完成到对象的绑定,但是在某些遗留系统中,前端传递的参数与后端Form表单定义的命名不会相同,例如在某些系统中参数为qp.page=1&qp.pageSize=50,而后端的Form表单类属性命名不可能带有点号,这时候我们可以自定义一个ArgumentResolver来自己设置参数对象。
例如,我们的query方法签名如下,QueryParamForm中的属性名称为page、pageSize:
@RequestMapping("/dtList")@ResponseBodypublic 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)@Documentedpublic @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对象的属性中。
@Componentpublic 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; }}
- 使用aspectj拦截器
以上面那个例子为背景,如果要做全局的参数校验,没必要在每个方法中主动写方法,可以利用AspectJ与Spring的集成,编入指定类到方法上的AOP切面,统一来做验证。详细代码如下:
@Component@Aspectpublic 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(); }}
- 全局错误处理,隐藏后端异常以及友好提示
通常情况下,一个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; } }}
- 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 {}
- 使用模板方法来简化代码开发
对于很多的相似逻辑,可以利用模板模式,把公共的操作封装到父类controller中。例如对于一个下载报表的需求,可以隐藏具体的写流等底层操作,将这些模板抽象化到父类BaseController中,子类只需要去实现传入一个调用获取报表数据Callback来,这和Hibernate的callback思想异曲同工。
@RequestMapping(value = "/downloadDtList")@ResponseBodypublic 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的源代码才感受的到其强大之处,希望你与我共勉,知其然还要知其所以然。
SpringMVC有三个层次,分别是:HttpServletBean 和 FrameworkServlet、DispatcherServlet
SpringMVC 实际上市基于方法(handler:Method)处理模式 所以它可以使用单例
Struts:实际上是基于类(class)处理模式
HttpServletBean 继承 JavaServlet当中的HttpServlet,其中的作用将servlet当中的参数把它设置到相应的属性,传递给spring的框架,springmvc上下文和spring跟环境是一个继承关系,所以通过我们的web容器实现servlet规范来执行这些spring的初始化,服务,销毁这一套
<!--spring基础配置--> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/SpringBeans.xml</param-value> </context-param> <context-param> <param-name>defaultHtmlEscape</param-name> <param-value>true</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <listener> <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class> </listener>
调用SpringBeans.xml
<import resource="classpath:dataResource_springBeans.xml" /> <import resource="classpath:user-dao-context.xml" /> <import resource="classpath:user-service-context.xml" /><mvc:view-controller path="/" view-name="forward:/sys/view/index"/> <mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>text/plain;charset=UTF-8</value> <value>text/html;charset=UTF-8</value> </list> </property> </bean> </mvc:message-converters> </mvc:annotation-driven>
用到的shiro管理user-shiro-context.xml、user-dao-context.xml
user-dao-context.xml
<beans> <!--用户--> <bean id="iUserDAO" class="com.zhijin.user.impl.UserDAO"/> <!--组织--> <bean id="iOrganizationDAO" class="com.zhijin.user.impl.OrganizationDAO"/> <!--用户日志--> <bean id="iUserLogDAO" class="com.zhijin.user.impl.UserLogDAO"/> <!--用户角色--> <bean id="iUserRoleDAO" class="com.zhijin.role.impl.UserRoleDAO"/></beans>
user-service-context.xml
<beans> <bean id="iUserService" class="com.zhijin.user.impl.UserService"> <property name="iUserDAO"><ref bean="iUserDAO"/></property> <property name="iESDockedDAO"><ref bean="iESDockedDAO"/></property> <property name="iUserRoleDAO"><ref bean="iUserRoleDAO"/></property> </bean> <bean id="iOrganizationService" class="com.zhijin.org.impl.OrganizationService"> <property name="iOrganizationDAO"><ref bean="iOrganizationDAO"/></property> </bean> <!--用户日志服务--> <bean id="iUserLogService" class="com.zhijin.user.impl.UserLogService"> <property name="iUserLogDAO"><ref bean="iUserLogDAO"/></property> </bean></beans>
user-shiro-context.xml
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/page/user/login.jsp" /> <property name="unauthorizedUrl" value="/page/user/login.jsp" /> <property name="filters"> <map> <entry key="authc"> <bean class="com.zhijin.user.shiro.SimpleFormAuthenticationFilter" /> </entry> </map> </property> <property name="filterChainDefinitions"> <value> 存放你所配置的信息 <!--/api/** = authc--> </value> </property> </bean> <!-- /view/developer/index.jsp = authc, perms[/view/developer/index.jsp] --><!-- perms 表示需要该权限才能访问的页面 --> <!-- /view/cooper/index.jsp = authc, perms[/view/cooper/index.jsp] --> <!-- /home = authc, perms[/home] --> <bean id="shiroRealm" class="com.zhijin.user.shiro.ShiroRealm"> <property name="iUserService" ref="iUserService" /> <property name="iOrganizationService" ref="iOrganizationService" /> </bean> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="shiroRealm"></property> <property name="cacheManager" ref="shiroEhcacheManager"/> </bean> <bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile"> <value>classpath:ehcache-shiro.xml</value> </property> </bean> <!-- 保证实现了Shiro内部lifecycle函数的bean执行 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
dataResource_springBeans.xml
链接数据库
<?xml version="1.0" encoding="UTF-8" standalone="no"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd"> <!--定时驱动打开--> <task:annotation-driven /> <context:component-scan base-package="com.zhijin"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <bean id="zjProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean"> <property name="singleton" value="true" /> <property name="ignoreResourceNotFound" value="true" /> <property name="locations"> <list> <value>/WEB-INF/config/db/jdbc.postgres.properties</value> <value>/WEB-INF/config/config.properties</value> </list> </property> </bean> <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE"/> <property name="ignoreResourceNotFound" value="true"/> <property name="ignoreUnresolvablePlaceholders" value="true"/> <property name="properties" ref="zjProperties" /> </bean> <bean id="defaultPersistenceUnitManager" class="org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager"> <property name="persistenceXmlLocation" value="/WEB-INF/persistence.xml"/> <!-- comment dataSourceLooup to use jndi --> <property name="dataSourceLookup"> <bean class="org.springframework.jdbc.datasource.lookup.BeanFactoryDataSourceLookup" /> </property> </bean> <!--<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"--> <!--p:location="/WEB-INF/config/db/jdbc.postgres.properties" />--> <tx:annotation-driven transaction-manager="jpaTransactionManager"/> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.databaseurl}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean><!-- <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!– 指定连接数据库的驱动–> <property name="driverClass" value="${jdbc.driverClassName}"/> <!– 指定连接数据库的URL–> <property name="jdbcUrl" value="${jdbc.databaseurl}"/> <!– 指定连接数据库的用户名–> <property name="user" value="${jdbc.username}"/> <!– 指定连接数据库的密码–> <property name="password" value="${jdbc.password}"/> <!– 指定连接池中保留的最大连接数. Default:15–> <property name="maxPoolSize" value="${jdbc.maxPoolSize}"/> <!– 指定连接池中保留的最小连接数–> <property name="minPoolSize" value="${jdbc.minPoolSize}"/> <!– 指定连接池的初始化连接数 取值应在minPoolSize 与 maxPoolSize 之间.Default:3–> <property name="initialPoolSize" value="${jdbc.initialPoolSize}"/> <!– 最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃。 Default:0–> <property name="maxIdleTime" value="${jdbc.maxIdleTime}"/> <!– 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数. Default:3–> <property name="acquireIncrement" value="${jdbc.acquireIncrement}"/> <!– JDBC的标准,用以控制数据源内加载的PreparedStatements数量。 但由于预缓存的statements属于单个connection而不是整个连接池所以设置这个参数需要考虑到多方面的因数.如果maxStatements与maxStatementsPerConnection均为0,则缓存被关闭。Default:0–> <property name="maxStatements" value="${jdbc.maxStatements}"/> <!– 每60秒检查所有连接池中的空闲连接.Default:0 –> <property name="idleConnectionTestPeriod" value="${jdbc.idleConnectionTestPeriod}"/> </bean>--> <bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="showSql" value="true"/> <property name="database" value="POSTGRESQL"/> </bean> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="persistenceUnitManager" ref="defaultPersistenceUnitManager" /> <property name="persistenceUnitName" value="postgre" /> <property name="jpaVendorAdapter" ref="jpaVendorAdapter"/> <!--<property name="packagesToScan" value="com.zhijin.**.entity"/>--> </bean> <bean id="jpaTransactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory" /> <property name="jpaDialect"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" /> </property> </bean> <!--sql service数据库配置--> <tx:annotation-driven transaction-manager="jpaTransactionManagerSql"/> <bean id="dataSourceSql" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driverClassName.sql}"/> <property name="url" value="${jdbc.databaseurl.sql}"/> <property name="username" value="${jdbc.username.sql}"/> <property name="password" value="${jdbc.password.sql}"/> </bean> <bean id="jpaVendorAdapterSql" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="showSql" value="false"/> <property name="databasePlatform" value="com.zhijin.base.MySQLServerDialect" /> <property name="database" value="POSTGRESQL"/> </bean> <bean id="entityManagerFactorySql" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="persistenceUnitManager" ref="defaultPersistenceUnitManager" /> <property name="persistenceUnitName" value="sqlUnit" /> <property name="jpaVendorAdapter" ref="jpaVendorAdapterSql"/> <property name="jpaPropertyMap"> <props> <prop key="hibernate.hbm2ddl.auto">none</prop> </props> </property> <!--<property name="packagesToScan" value="com.zhijin.**.entity"/>--> </bean> <bean id="jpaTransactionManagerSql" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactorySql" /> <property name="jpaDialect"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" /> </property> </bean> <!--日志处理切面--> <bean id="myLog" class="com.zhijin.aspect.UserLogAspect"> <property name="iUserLogService" ref="iUserLogService"></property> </bean> <!--ES数据对接切面--> <bean id="esDataHandleAspect" class="com.zhijin.aspect.EsDataHandleAspect"> </bean> <bean id="registedSchoolInfo" class="com.zhijin.aspect.RegistedSchoolInfoAspect"/> <!--将日志类注入到bean中。--> <!--业务层自动提交事物配置--> <aop:config> <aop:pointcut id="crudMethods" expression="execution(* com.zhijin.*.*.*.*(..))"/> <aop:pointcut id="insertMethodsSql" expression="execution(* com.zhijin.esdocked.impl.ESDockedService.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="crudMethods"/> <aop:advisor advice-ref="txAdviceSql" pointcut-ref="insertMethodsSql"/> <aop:aspect id="logAspect" ref="myLog"> <aop:pointcut id="log" expression="execution(* com.zhijin.*.impl.*Service.*(..))"/><!--配置在log包下所有的类在调用之前都会被拦截--> <aop:after method="after" pointcut-ref="log"></aop:after> </aop:aspect> <aop:aspect id="registedAspect" ref="registedSchoolInfo"> <aop:pointcut id="regAspect" expression="execution(* com.zhijin.student.impl.StudentEnrollDAO.*(..))"/> <aop:after method="after" pointcut-ref="regAspect"></aop:after> </aop:aspect> <aop:aspect ref="esDataHandleAspect" id="es"> <aop:pointcut id="esDataHandlePointcut" expression="execution(* com.zhijin.*.impl.StudentService.*(..))"></aop:pointcut> <aop:after method="after" pointcut-ref="esDataHandlePointcut"></aop:after> </aop:aspect> </aop:config> <tx:advice id="txAdvice" transaction-manager="jpaTransactionManager"> <tx:attributes> <tx:method name="save*" propagation="REQUIRED"/> <tx:method name="add*" propagation="REQUIRED"/> <tx:method name="edit*" propagation="REQUIRED"/> <tx:method name="delete*" propagation="REQUIRED"/> <tx:method name="update*" propagation="REQUIRED"/> <tx:method name="submit*" propagation="REQUIRED"/> <tx:method name="audit*" propagation="REQUIRED"/> <tx:method name="batch*" propagation="REQUIRED"/> <tx:method name="execute*" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <tx:advice id="txAdviceSql" transaction-manager="jpaTransactionManagerSql"> <tx:attributes> <tx:method name="insert*" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <!-- <bean id="defaultCacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> <property name="configLocation" value="classpath:ehcache.xml"/> </bean> <bean id="ehCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean"> <property name="cacheManager" ref="defaultCacheManager"/> <property name="cacheName" value="DEFAULT_CACHE"/> </bean> <bean id="methodCacheInterceptor" class="com.zhijin.util.MethodCacheInterceptor"> <property name="cache" ref="ehCache"/> </bean> <bean id="methodCachePointCut" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="advice" ref="methodCacheInterceptor" /> <property name="patterns"> <list> <value>com.zhijin.esdocked.api.IESDockedService.get.*</value> </list> </property> </bean> --> <!--<jpa:repositories base-package="com.jcg.examples.repo" />--></beans>
用到shiro的cache-shiro.xml
<?xml version="1.0" encoding="UTF-8"?><ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" name="shiroEhcacheManager"> <diskStore path="java.io.tmpdir/education/shiro" /> <defaultCache maxElementsInMemory="10000" eternal="true" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" diskSpoolBufferSizeMB="30" maxElementsOnDisk="10000000" diskPersistent="false" diskExpiryThreadIntervalSeconds="120"/></ehcache>
<servlet> <servlet-name>rest</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/webmvc-config.xml</param-value> </init-param> <load-on-startup>2</load-on-startup> </servlet> <servlet-mapping> <servlet-name>rest</servlet-name> <url-pattern>/api/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>rest</servlet-name> <url-pattern>/manage/*</url-pattern> </servlet-mapping> <!-- 使用restFul支持put请求 -->
<!--shiro配置--> <filter> <filter-name>shiroFilter</filter-name> <filter-class> org.springframework.web.filter.DelegatingFilterProxy </filter-class> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
之后调用webmvc-config.xml
<context:annotation-config /><mvc:annotation-driven conversion-service="conversionService"/><bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <set> <bean class="com.zhijin.converter.TimestampConverter"></bean> </set> </property></bean><context:component-scan base-package="com.zhijin" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/></context:component-scan><mvc:default-servlet-handler />
<!-- SpringMVC上传文件时,需要配置MultipartResolver处理器 --> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="defaultEncoding" value="UTF-8"/> <!-- 指定所上传文件的总大小不能超过2M。注意maxUploadSize属性的限制不是针对单个文件,而是所有文件的容量之和 --> <!-- 协议附件需要设置不超过50M --> <property name="maxUploadSize" value="104857600"/> </bean>
- SpringMVC源码分析,springMVC原理
- SpringMVC源码分析系列
- SpringMVC源码分析系列
- springmvc源码分析
- SpringMVC源码分析系列
- springMVC源码分析--HandlerMethod
- springMVC源码分析--ModelFactory
- 一步步分析SpringMVC源码
- SpringMVC源码分析
- SpringMVC源码分析系列
- SpringMVC源码分析
- springmvc----源码分析之springmvc执行流程
- SpringMVC加载WebApplicationContext源码分析
- SpringMVC加载WebApplicationContext源码分析
- springmvc 加载WebApplicationContext源码分析
- SpringMVC-前端控制器源码分析
- SpringMVC加载WebApplicationContext源码分析
- SpringMVC加载WebApplicationContext源码分析
- 如何上传发布自己的npm组件包
- java服务端接入有赞,实现后台登陆有赞商城的需求
- 51nod 1283 最小周长
- cmd操作
- HBase-客户端重试机制
- SpringMVC源码分析
- Struts2文件下载
- linux c开发: 程序崩溃时保存堆栈信息并解析具体代码行
- 原生javascript进行class的增加 删除操作
- Xcode CocoaPods vim文件编辑快捷键
- MATLAB 自定义函数拟合
- 拼凑钱币
- Centos7 修改/新增ssh默认端口
- 同时安装python2.7和Anaconda后python.exe和pip.py的区分