Spring MVC本身对Restful支持非常好。它的@RequestMapping、@RequestParam、@PathVariable、@ResponseBody注解很好的支持了REST。18.2 Creating RESTful services
1. @RequestMapping
Spring uses the @RequestMapping method annotation to define the URI Template for the request. 类似于struts的action-mapping。 可以指定POST或者GET。
2. @PathVariable
The @PathVariable method parameter annotation is used to indicate that a method parameter should be bound to the value of a URI template variable. 用于抽取URL中的信息作为参数。(注意,不包括请求字符串,那是@RequestParam做的事情。)
1 2 3 4
@RequestMapping("/owners/{ownerId}", method=RequestMethod.GET) public String findOwner(@PathVariable String ownerId, Model model) { // ... }
如果变量名与pathVariable名不一致,那么需要指定:
1 2 3 4
@RequestMapping("/owners/{ownerId}", method=RequestMethod.GET) public String findOwner(@PathVariable("ownerId") String theOwner, Model model) { // implementation omitted }
Tip
method parameters that are decorated with the @PathVariable annotation can be of any simple type such as int, long, Date… Spring automatically converts to the appropriate type and throws a TypeMismatchException if the type is not correct.
// Indicate whether the given class is supported by this converter. booleansupports(Class<? extends T> clazz);
// Return the list of MediaType objects supported by this converter. List<MediaType> getSupportedMediaTypes();
// Read an object of the given type form the given input message, and returns it. T read(Class<T> clazz, HttpInputMessage inputMessage)throws IOException, HttpMessageNotReadableException;
// Write an given object to the given output message. voidwrite(T t, HttpOutputMessage outputMessage)throws IOException, HttpMessageNotWritableException;
}
Spring MVC对HttpMessageConverter有多种默认实现,基本上不需要自己再自定义HttpMessageConverter
StringHttpMessageConverter - converts strings
FormHttpMessageConverter - converts form data to/from a MultiValueMap<String, String>
ByteArrayMessageConverter - converts byte arrays
SourceHttpMessageConverter - convert to/from a javax.xml.transform.Source
// ignore some setter and getter... publicvoidsetMediaTypes(Map<String, String> mediaTypes) { Assert.notNull(mediaTypes, "'mediaTypes' must not be null"); for (Map.Entry<String, String> entry : mediaTypes.entrySet()) { Stringextension= entry.getKey().toLowerCase(Locale.ENGLISH); MediaTypemediaType= MediaType.parseMediaType(entry.getValue()); this.mediaTypes.put(extension, mediaType); } } publicvoidsetDefaultViews(List<View> defaultViews) { this.defaultViews = defaultViews; } publicvoidsetDefaultContentType(MediaType defaultContentType) { this.defaultContentType = defaultContentType; } publicvoidsetViewResolvers(List<ViewResolver> viewResolvers) { this.viewResolvers = viewResolvers; }
@Override protectedvoidinitServletContext(ServletContext servletContext) { if (this.viewResolvers == null) { Map<String, ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class); this.viewResolvers = newArrayList<ViewResolver>(matchingBeans.size()); for (ViewResolver viewResolver : matchingBeans.values()) { if (this != viewResolver) { this.viewResolvers.add(viewResolver); } } } if (this.viewResolvers.isEmpty()) { logger.warn("Did not find any ViewResolvers to delegate to; please configure them using the " + "'viewResolvers' property on the ContentNegotiatingViewResolver"); } OrderComparator.sort(this.viewResolvers); } public View resolveViewName(String viewName, Locale locale)throws Exception { RequestAttributesattrs= RequestContextHolder.getRequestAttributes(); Assert.isInstanceOf(ServletRequestAttributes.class, attrs); List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest()); if (requestedMediaTypes != null) { List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes); ViewbestView= getBestView(candidateViews, requestedMediaTypes); if (bestView != null) { return bestView; } } if (this.useNotAcceptableStatusCode) { if (logger.isDebugEnabled()) { logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code"); } return NOT_ACCEPTABLE_VIEW; } else { logger.debug("No acceptable view found; returning null"); returnnull; } }
protected List<MediaType> getMediaTypes(HttpServletRequest request) { if (this.favorPathExtension) { StringrequestUri= urlPathHelper.getRequestUri(request); Stringfilename= WebUtils.extractFullFilenameFromUrlPath(requestUri); MediaTypemediaType= getMediaTypeFromFilename(filename); if (mediaType != null) { if (logger.isDebugEnabled()) { logger.debug("Requested media type is '" + mediaType + "' (based on filename '" + filename + "')"); } return Collections.singletonList(mediaType); } } if (this.favorParameter) { if (request.getParameter(this.parameterName) != null) { StringparameterValue= request.getParameter(this.parameterName); MediaTypemediaType= getMediaTypeFromParameter(parameterValue); if (mediaType != null) { if (logger.isDebugEnabled()) { logger.debug("Requested media type is '" + mediaType + "' (based on parameter '" + this.parameterName + "'='" + parameterValue + "')"); } return Collections.singletonList(mediaType); } } } if (!this.ignoreAcceptHeader) { StringacceptHeader= request.getHeader(ACCEPT_HEADER); if (StringUtils.hasText(acceptHeader)) { try { List<MediaType> mediaTypes = MediaType.parseMediaTypes(acceptHeader); MediaType.sortByQualityValue(mediaTypes); if (logger.isDebugEnabled()) { logger.debug("Requested media types are " + mediaTypes + " (based on Accept header)"); } return mediaTypes; } catch (IllegalArgumentException ex) { if (logger.isDebugEnabled()) { logger.debug("Could not parse accept header [" + acceptHeader + "]: " + ex.getMessage()); } returnnull; } } } if (this.defaultContentType != null) { if (logger.isDebugEnabled()) { logger.debug("Requested media types is " + this.defaultContentType + " (based on defaultContentType property)"); } return Collections.singletonList(this.defaultContentType); } else { return Collections.emptyList(); } }
protected MediaType getMediaTypeFromFilename(String filename) { Stringextension= StringUtils.getFilenameExtension(filename); if (!StringUtils.hasText(extension)) { returnnull; } extension = extension.toLowerCase(Locale.ENGLISH); MediaTypemediaType=this.mediaTypes.get(extension); if (mediaType == null && this.useJaf && jafPresent) { mediaType = ActivationMediaTypeFactory.getMediaType(filename); if (mediaType != null) { this.mediaTypes.putIfAbsent(extension, mediaType); } } return mediaType; } protected MediaType getMediaTypeFromParameter(String parameterValue) { returnthis.mediaTypes.get(parameterValue.toLowerCase(Locale.ENGLISH)); } private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) throws Exception { List<View> candidateViews = newArrayList<View>(); for (ViewResolver viewResolver : this.viewResolvers) { Viewview= viewResolver.resolveViewName(viewName, locale); if (view != null) { candidateViews.add(view); } for (MediaType requestedMediaType : requestedMediaTypes) { List<String> extensions = getExtensionsForMediaType(requestedMediaType); for (String extension : extensions) { StringviewNameWithExtension= viewName + "." + extension; view = viewResolver.resolveViewName(viewNameWithExtension, locale); if (view != null) { candidateViews.add(view); } } } } if (!CollectionUtils.isEmpty(this.defaultViews)) { candidateViews.addAll(this.defaultViews); } return candidateViews; } private List<String> getExtensionsForMediaType(MediaType requestedMediaType) { List<String> result = newArrayList<String>(); for (Entry<String, MediaType> entry : this.mediaTypes.entrySet()) { if (requestedMediaType.includes(entry.getValue())) { result.add(entry.getKey()); } } return result; } private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes) { MediaTypebestRequestedMediaType=null; ViewbestView=null; for (MediaType requestedMediaType : requestedMediaTypes) { for (View candidateView : candidateViews) { if (StringUtils.hasText(candidateView.getContentType())) { MediaTypecandidateContentType= MediaType.parseMediaType(candidateView.getContentType()); if (requestedMediaType.includes(candidateContentType)) { bestRequestedMediaType = requestedMediaType; bestView = candidateView; break; } } } if (bestView != null) { if (logger.isDebugEnabled()) { logger.debug("Returning [" + bestView + "] based on requested media type '" + bestRequestedMediaType + "'"); } break; } } return bestView; } ...
}
可以看到ContentNegotiationViewResolver有点类似于ComposeCommand(参见Command模式 by GoF),它本身实现了ViewResolver接口,所以它是一个ViewResolver,但是它组合了一堆的ViewResolver,根据一定的规则(前面讨论的content negotiation)将视图请求转发给最match的ViewResolver。
This view resolver uses the requested media type to select a suitable View for a request. This media type is determined by using the following criteria:
If the requested path has a file extension and if the setFavorPathExtension(boolean) property is true, the mediaTypes property is inspected for a matching media type.
If the request contains a parameter defining the extension and if the setFavorParameter(boolean) property is true, the mediaTypes property is inspected for a matching media type. The default name of the parameter is format and it can be configured using the parameterName property.
If there is no match in the mediaTypes property and if the Java Activation Framework (JAF) is both enabled and present on the classpath, FileTypeMap.getContentType(String) is used instead.
If the previous steps did not result in a media type, and ignoreAcceptHeader is false, the request Accept header is used.
Once the requested media type has been determined, this resolver queries each delegate view resolver for a View and determines if the requested media type is compatible with the view’s content type). The most compatible view is returned.
这个就是上面提到的Spring MVC默认的三个ContentNegotiationStrategy,即所谓的PPA Strategy(path extension, then parameter, then Accept header) ,顺序也是先path extension,然后parameter(默认是format参数),然后才是accept头。
The ContentNegotiatingViewResolver does not resolve views itself, but delegates to other ViewResolvers. By default, these other view resolvers are picked up automatically from the application context, though they can also be set explicitly by using the viewResolvers property. Note that in order for this view resolver to work properly, the order property needs to be set to a higher precedence than the others (the default is Ordered.HIGHEST_PRECEDENCE.)
Additionally, this view resolver exposes the defaultViews property, allowing you to override the views provided by the view resolvers. Note that these default views are offered as candicates, and still need have the content type requested (via file extension, parameter, or Accept header, described above). You can also set the default content type directly, which will be returned when the other mechanisms (Accept header, file extension or parameter) do not result in a match.
方法名称很有规律,都是这个pattern——${HTTP Method}${WhatIsReturne}。例如getForObject() will perform a GET, convert the HTTP response into an object type of your choice, and returns that object. postForLocation will do a POST, converting the given object into a HTTP request, and returns the response HTTP Location header where the newly created object can be found. As you can see, these methods try to enforce REST best practices.
The map variant expands the template based on variable name, and is therefore more useful when using many variables, or when a single variable is used multiple times.
<!-- Forwards requests to the "/" resource to the "welcome" view --> <mvc:view-controllerpath="/"view-name="index"/>
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources/ directory --> <mvc:resourcesmapping="/resources/**"location="/resources/" /> <!-- 注意:配置了mvc:resources就必须配置这个选项,否则handler mapping都失效了 @see http://stackoverflow.com/questions/7910845/the-handler-mapping-from-the-mvcresource-override-other-mappings-which-defined --> <mvc:annotation-driven />