该帖已经被评为精华帖
|
|
---|---|
作者 | 正文 |
发表时间:2010-11-29
最后修改:2011-02-01
最近拿Spring MVC 3.x做项目,用了最新的系列相关Annotation来做Controller,有几点心得体会值得分享。
转载请注明 :IT进行时(zhengxianquan AT hotmail.com) from http://itstarting.iteye.com/
一、编写一个AbstractController.java,所有的Controller必须扩展 除了获得Template Design Pattern的好处之外,还能一点,那就是放置全局的@InitBinder: public class AbstractController { ... @InitBinder protected void initBinder(WebDataBinder binder) { SimpleDateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy"); dateFormat.setLenient(false); binder.registerCustomEditor(Date.class,new CustomDateEditor(dateFormat, false)); } ... }
二、每个域都应该有一个Controller,且做好URI规划 大家都知道Spring MVC 3.x是完全支持Restful的,我们把URI做好规划,对于诸如ACL的实现会有很大的帮助。建议的URI规划如下:{Domain}[/{SubDomain}]/{BusinessAction}/{ID}。比如: hotels/bookings/cancel/{id} ——表示此URI匹配hotels域的bookings子域,将要进行的是取消某项booking的操作。代码如下: @Controller @RequestMapping(value = "/hotels") public class HotelsController extends AbstractController { ... @RequestMapping(value = "/bookings/cancel/{id}", method = RequestMethod.POST) public String deleteBooking(@PathVariable long id) { bookingService.cancelBooking(id); //use prefix 'redirect' to avoid duplicate submission return "redirect:../../search"; } ... }
另外还有几个重要原因: 1、由于Spring的DefaultAnnotationHandlerMapping.java在做Mapping的时候,先做是否有类匹配,再找方法,把基本的mapping放在类上面,可以加速匹配效率; 2、后续可以通过更细腻的支持Ant path style的AntPathMatcher来规划URI Template资源,做ACL控制(请参考后面的心得体会)。
三、JSON/XML等ajax的支持很cool,可以尝试 JSON/XML/RSS等均可支持,当然有些denpendency,比如JSON的默认支持,需要jackson jar出现在lib中,POM的artifact如下: <dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-mapper-asl</artifactId> <version>1.5.3</version> </dependency> 这样,我们其实根本就不需要进行额外的JSON转换了,Spring MVC 3会根据请求的格式自行转换: @ResponseBody @RequestMapping(value = "/ajax", method = RequestMethod.POST) public JsonDataWrapper<Hotel> ajax(WebRequest request, Hotel hotel, Model model) throws Exception { JsonDataWrapper<Hotel> jsonDataWrapper = this.getPaginatedGridData(request, hotel, hotelService); return jsonDataWrapper; } 注 :我上面的JsonDataWrapper只是我自己做的一个简单的wrapper,目的是为jQuery Flexigrid plugin做数据准备的。还是贴出来吧: /** * A wrapper class for jQuery Flexigrid plugin component. * * The format must be like this: * <code> * {"total":2,"page":1,"rows":[ * {"personTitle":"Mr","partyName":"Test8325","personDateOfBirth":"1970-07-12"}, * {"personTitle":"Ms","partyName":"Ms Susan Jones","personDateOfBirth":"1955-11-27"} * ]} * </code> * * @author bright_zheng * * @param <T>: the generic type of the specific domain */ public class JsonDataWrapper<T> implements Serializable { private static final long serialVersionUID = -538629307783721872L; public JsonDataWrapper(int total, int page, List<T> rows){ this.total = total; this.page = page; this.rows = rows; } private int total; private int page; private List<T> rows; public int getTotal() { return total; } public void setTotal(int total) { this.total = total; } public int getPage() { return page; } public void setPage(int page) { this.page = page; } public List<T> getRows() { return rows; } public void setRows(List<T> rows) { this.rows = rows; } }
四、Controller的单元测试变得很可为且简单 以前的项目从来不做controller层的测试,用了Spring MVC 3这一点就不再难为情,做吧: public class HotelsControllerTest { private static HandlerMapping handlerMapping; private static HandlerAdapter handlerAdapter; private static MockServletContext msc; @BeforeClass public static void setUp() { String[] configs = { "file:src/main/resources/context-*.xml", "file:src/main/webapp/WEB-INF/webapp-servlet.xml" }; XmlWebApplicationContext context = new XmlWebApplicationContext(); context.setConfigLocations(configs); msc = new MockServletContext(); context.setServletContext(msc); context.refresh(); msc.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, context); ApplicationContextManager manager = new ApplicationContextManager(); manager.setApplicationContext(context); handlerMapping = (HandlerMapping) ApplicationContextManager.getContext().getBean(DefaultAnnotationHandlerMapping.class); handlerAdapter = (HandlerAdapter) ApplicationContextManager.getContext().getBean(ApplicationContextManager.getContext().getBeanNamesForType(AnnotationMethodHandlerAdapter.class)[0]); } @Test public void list() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletResponse response = new MockHttpServletResponse(); request.setRequestURI("/hotels"); request.addParameter("booking.id", "1002"); request.addParameter("hotel.name", ""); request.setMethod("POST"); //HandlerMapping HandlerExecutionChain chain = handlerMapping.getHandler(request); Assert.assertEquals(true, chain.getHandler() instanceof HotelsController); //HandlerAdapter final ModelAndView mav = handlerAdapter.handle(request, response, chain.getHandler()); //Assert logic Assert.assertEquals("hotels/search", mav.getViewName()); } } 需要说明一下 :由于当前最想版本的Spring(Test) 3.0.5还不支持@ContextConfiguration的注解式context file注入,所以还需要写个setUp处理下,否则类似于Tiles的加载过程会有错误,因为没有ServletContext。3.1的版本应该有更好的解决方案,参见:https://jira.springsource.org/browse/SPR-5243 。 Service等其他layer就没有这类问题,测试的写法将变得更加优雅,贴个出来: @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"file:src/main/resources/context-*.xml" }) public class DefaultServiceImplTest { /** @Autowired works if we put @ContextConfiguration at junit type */ @Autowired @Qualifier("hotelService") private HotelService<Hotel> hotelService; @Test public void insert() { Hotel hotel = new Hotel(); hotel.setAddress("addr"); hotel.setCity("Singapore"); hotel.setCountry("Singapore"); hotel.setName("Great Hotel"); hotel.setPrice(new BigDecimal(200)); hotel.setState("Harbarfront"); hotel.setZip("010024"); hotelService.insert(hotel); } } 五、ACL可以写一个HandlerInterceptorAdapter,在“事前”拦截。 由于Spring是Restful friendly的framework,做ACL就不要用过滤器了,用interceptor吧: public class AclInterceptor extends HandlerInterceptorAdapter { private static final Logger logger = Logger.getLogger(AclInterceptor.class); /** default servlet prefix */ private static final String DEFAULT_SERVLET_PREFIX = "/servlet"; /** will be injected from context configuration file */ private AclService service; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String currentUri = request.getRequestURI(); boolean isAccessible = true; //only intercept for annotated business controllers Controller c = AnnotationUtils.findAnnotation(handler.getClass(), Controller.class); if(c!=null){ String[] grantedResource = getGrantedResource(request); if(grantedResource==null || grantedResource.length==0){ throw new AccessDeniedException("No resource granted"); } isAccessible = service.isAccessible(grantedResource, currentUri); if(logger.isDebugEnabled()){ logger.debug("ACL interceptor excueted. Accessible for Uri[" + currentUri +"] = " + isAccessible); } //if isAccessible==true, throw custom AccessDeniedException if(!isAccessible) throw new AccessDeniedException(); } return isAccessible; } /** * transfer the original Uri to resource Uri * e.g.: * original Uri: /servlet/hotels/ajax * target Uri : /hotels/ajax * @param originalUri * @return */ protected String getUri(String originalUri){ return originalUri.substring(DEFAULT_SERVLET_PREFIX.length()); } /** * Get the granted resource from session * @param request * @return */ protected String[] getGrantedResource(HttpServletRequest request){ //get the resources from current session //String[] uriResourcePattern = (String[]) request.getSession().getAttribute("uriResourcePattern"); //TODO: mock data here String[] uriResourcePattern = new String[]{"/**"}; return uriResourcePattern; } public void setService(AclService service) { this.service = service; } } 注 :上面还有TODO部分,很简单,登录后把当然用户可访问的Ant path style的URI Resources放到session中,作为一个String[],这里拿来出匹配即可,建议匹配就用org.springframework.util.AntPathMatcher。
六、对于Ajax Form Validation,正在寻求更佳的解禁方案。 Spring 3已经支持@Valid的注解式validation,又一个JSR,但通过我研究代码发现,默认的org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.java实现还是非常封闭,并不能很好的实现binding后执行业务代码前处理,所以目前的实现办法还不算优雅。 已经实现,大致的思路是: 1)发出ajax form post请求(用jQuery); 2)正常mapping到controller的方法中处理; 3)做form validation; 4)不管怎样,forward到一个解析错误消息的JSP,叫ajaxFormValidatorResult.jsp; 5)再写一个jquery plugin,叫errorBinder.js,解析错误并自定binding为Tooltip,如果没错误就正常submit 贴个简单的demo效果,欢迎拍砖。 ==Before
补充说明:后面陆续补充的心得,就不在这里出现了,而是以跟帖的形式存在,便于讨论吧
声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2010-11-30
Controller的单元测试那的ApplicationContextManager 用的是unitils 哪个版本的?
如果可以把单元测试写得更清楚点就好了。 |
|
返回顶楼 | |
发表时间:2010-11-30
请教:
我目前采用的是ContentNegotiatingViewResolver,即通过后缀名分类不同的请求和响应。是HTML的还是JSON。与此同时出现的问题就是异常处理,好像SpringMVC在这方面支持的不够,尤其是Controller方法直接抛出异常的情况,在HTML的情况下应跳转到error页面;而在JSON请求时应返回异常的JSON格式数据。我都是扩展了源码才达到了效果 这方面你是怎么处理的? 另外直接的@ResponseBody也不是都有效的,多对象响应时就不灵活了 |
|
返回顶楼 | |
发表时间:2010-11-30
itstarting 写道 注 :上面还有TODO部分,很简单,登录后把当然用户可访问的Ant path style的URI Resources放到session中,作为一个String[],这里拿来出匹配即可,建议匹配就用org.springframework.util.AntPathMatcher。 你这种ACL对源码侵入比较小,呵呵,不过操作起来好象不怎么方便和直观。 可以看下这篇文章 http://jiangshaolin.iteye.com/blog/780375 |
|
返回顶楼 | |
发表时间:2010-11-30
和Form的绑定比较直观,可以参考http://sarin.iteye.com/blog/829738,其中还附带了对Security的整合。
|
|
返回顶楼 | |
发表时间:2010-11-30
楼上的,老实说我也看了最新的Spring Security,我想我只是需要控制URI资源,不需要做复杂的方法级安全控制之类的,所以放弃使用了
我想我的思路算不上创新,但没有比这个更简单的做法了吧,我想 BTW:你转发的那个帖子很牛,很。。。。。。。。。。。。。。。。。。。。长,抱歉我没看完 |
|
返回顶楼 | |
发表时间:2010-11-30
littcai 写道 请教:
我目前采用的是ContentNegotiatingViewResolver,即通过后缀名分类不同的请求和响应。是HTML的还是JSON。与此同时出现的问题就是异常处理,好像SpringMVC在这方面支持的不够,尤其是Controller方法直接抛出异常的情况,在HTML的情况下应跳转到error页面;而在JSON请求时应返回异常的JSON格式数据。我都是扩展了源码才达到了效果 这方面你是怎么处理的? 另外直接的@ResponseBody也不是都有效的,多对象响应时就不灵活了 异常处理可以用ExceptionHandler,当然这里处理的是“全局”的异常,不要太多,比如未登录异常、系统技术性异常(总要说声Sorry吧),几个就够了,view层面的异常用户关心这么多干嘛;如果不是全局的异常,在自己的Controller内私了,加上一个或多个处理私有异常的方法,可以加上Spring提供的专门的Annotation,叫@ExceptionHandler 我不知道你说的是不是form validation的“异常”,这是另外的话题,要讨论的话,也很长很值得 源码老实说,有些地方还有TODO的痕迹,不要太强求,实在不行就跟你那样扩展,但绝对不建议,毕竟绝大部分扩展点,Spring MVC还是留着的,设计已经很棒了——回头看看可怜的Struts吧 |
|
返回顶楼 | |
发表时间:2010-11-30
楼主写得不错啊,我也正在学spring MVC 3
不过,我看了一下spring 3 API,发现很多Controler都变为Deprecated,建议用注解代替。但不清楚AbstractWizardFormController这个怎样用注解来代替?请教楼主了…… |
|
返回顶楼 | |
发表时间:2010-12-01
kongruxi 写道 楼主写得不错啊,我也正在学spring MVC 3
不过,我看了一下spring 3 API,发现很多Controler都变为Deprecated,建议用注解代替。但不清楚AbstractWizardFormController这个怎样用注解来代替?请教楼主了…… 确实,很多原先的Controller接口都不再鼓励使用,他们认为Controller其实就应该与一般的POJO没啥区别,TDD的意味非常明显 AbstractWizardFormController这种wizard-style的东西,现在鼓励用Spring Webflow,这是基于Spring MVC之上的关注会话Scope的web解决方案,也还不错,但这个框架太多的conventions,我反而不是特别的喜欢——我隔壁有一个大型项目(何谓大,项目金额1亿人民币)全部用Spring Webflow,完全不要纯粹的Spring MVC,我有点厌恶这样的架构师,简直是疯子,工作量明显加大了——我喜欢这两者mix |
|
返回顶楼 | |
发表时间:2010-12-01
littcai 写道 请教:
我目前采用的是ContentNegotiatingViewResolver,即通过后缀名分类不同的请求和响应。是HTML的还是JSON。与此同时出现的问题就是异常处理,好像SpringMVC在这方面支持的不够,尤其是Controller方法直接抛出异常的情况,在HTML的情况下应跳转到error页面;而在JSON请求时应返回异常的JSON格式数据。我都是扩展了源码才达到了效果 这方面你是怎么处理的? 另外直接的@ResponseBody也不是都有效的,多对象响应时就不灵活了 我也是用ContentNegotiatingViewResolver,而且我觉得异常处理也很方便,只要用@ExcepionHandler或者HandlerExceptionResolver就可以了。 而且我多视图的情况,我不建议使用@ResponseBody,还是返回自己的对象好 @RequestMapping(method = POST) @ModelAttribute @ResponseStatus(HttpStatus.CREATED) public MyResponse hello(@Valid MyRequest request) throws MyException { myService.hello(request); return new MyResponse(); } 还有在HandlerExceptionResolver中处理异常,指定视图名称就可以了。 @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { MyResponse myResponse = new MyResponse(); return new ModelAndView("error").addObject(myResponse); } |
|
返回顶楼 | |