SpringMVC之Restful

Spring MVC本身对Restful支持非常好。它的@RequestMapping@RequestParam@PathVariable@ResponseBody注解很好的支持了REST。

1. @RequestMapping

@RequestMapping 方法注解指定一个请求的URI映射地址. 类似于struts的action-mapping,同时可以指定POST或者GET请求类型。

@RequestMapping("/owners/{ownerId}", method=RequestMethod.GET)
public String findOwner(@PathVariable String ownerId, Model model) {
        // ...
}

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做的事情。)

@RequestMapping("/owners/{ownerId}", method=RequestMethod.GET)
public String findOwner(@PathVariable String ownerId, Model model) {
        // ...
}

如果变量名与pathVariable名不一致,那么需要指定:

@RequestMapping("/owners/{ownerId}", method=RequestMethod.GET)
public String findOwner(@PathVariable("ownerId") String theOwner, Model model) {
    // implementation omitted
}

注意: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.

3. @RequestParam

官方文档居然没有对这个注解进行说明,估计是遗漏了(真不应该啊)。这个注解跟@PathVariable功能差不多,只是参数值的来源不一样而已。它的取值来源是请求参数(querystring或者post表单字段)。对了,因为它的来源可以是POST字段,所以它支持更丰富和复杂的类型信息,比如文件对象。

@RequestMapping("/imageUpload")
public String processImageUpload(@RequestParam("name") String name,
                @RequestParam("description") String description,
                @RequestParam("image") MultipartFile image) throws IOException {
    this.imageDatabase.storeImage(name, image.getInputStream(),
                                    (int) image.getSize(), description);
    return "redirect:imageList";
}

还可以设置defaultValue:

@RequestMapping("/imageUpload")
public String processImageUpload(@RequestParam(value="name", defaultValue="arganzheng") String name,
                @RequestParam("description") String description,
                @RequestParam("image") MultipartFile image) throws IOException {
    this.imageDatabase.storeImage(name, image.getInputStream(),
                                    (int) image.getSize(), description);
    return "redirect:imageList";
}

4. @RequestBody@ResponseBody

这两个注解其实用到了Spring的一个非常灵活的设计——HttpMessageConverter  与@RequestParam不同,@RequestBody@ResponseBody是针对整个HTTP请求或者返回消息的。前者只是针对HTTP请求消息中的一个 name=value 键值对(名称很贴切)。

HtppMessageConverter负责将HTTP请求消息(HTTP request message)转化为对象,或者将对象转化为HTTP响应体(HTTP response body)。

public interface HttpMessageConverter<T> {

    // Indicate whether the given class is supported by this converter.
    boolean supports(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.
    void write(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
  • RssChannelHttpMessageConverter - convert to/from RSS feeds
  • MappingJacksonHttpMessageConverter - convert to/from JSON using Jackson‘s ObjectMapper
  • etc...

然而对于RESTful应用,用的最多的当然是MappingJacksonHttpMessageConverter,但是MappingJacksonHttpMessageConverter不是默认的HttpMessageConverter。

public class AnnotationMethodHandlerAdapter extends WebContentGenerator
implements HandlerAdapter, Ordered, BeanFactoryAware {

    ...

    public AnnotationMethodHandlerAdapter() {
        // no restriction of HTTP methods by default
        super(false);

        // See SPR-7316
        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
        stringHttpMessageConverter.setWriteAcceptCharset(false);
        this.messageConverters = new HttpMessageConverter[]{new ByteArrayHttpMessageConverter(), stringHttpMessageConverter,
        new SourceHttpMessageConverter(), new XmlAwareFormHttpMessageConverter()};
    }
} 

如上:默认的HttpMessageConverterByteArrayHttpMessageConverterstringHttpMessageConverterSourceHttpMessageConverterXmlAwareFormHttpMessageConverter转换器。所以需要配置一下:

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="messageConverters">
    <list>
        <bean class="org.springframework.http.converter.StringHttpMessageConverter">
        <property name="supportedMediaTypes">
            <list>
            <value>text/plain;charset=GBK</value>
            </list>
        </property>
        </bean>
        <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" />
    </list>
    </property>
</bean>

配置好了之后,就可以享受@Requestbody@ResponseBody对JONS转换的便利之处了:

@RequestMapping(value = "api", method = RequestMethod.POST)
@ResponseBody
public boolean addApi(@RequestBody
    Api api, @RequestParam(value = "afterApiId", required = false)
    Integer afterApiId) {
        Integer id = apiMetadataService.addApi(api);
        return id > 0;
}

@RequestMapping(value = "api/{apiId}", method = RequestMethod.GET)
@ResponseBody
public Api getApi(@PathVariable("apiId")
    int apiId) {
        return apiMetadataService.getApi(apiId, Version.primary);
}

一般情况下我们是不需要自定义HttpMessageConverter,不过对于Restful应用,有时候我们需要返回jsonp数据:

package me.arganzheng.study.springmvc.util;

import java.io.IOException;
import java.io.PrintStream;

import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

public class MappingJsonpHttpMessageConverter extends MappingJacksonHttpMessageConverter {

    public MappingJsonpHttpMessageConverter() {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setSerializationConfig(objectMapper.getSerializationConfig().withSerializationInclusion(Inclusion.NON_NULL));
    setObjectMapper(objectMapper);
    }

    @Override
    protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
    String jsonpCallback = null;

    RequestAttributes reqAttrs = RequestContextHolder.currentRequestAttributes();
    if(reqAttrs instanceof ServletRequestAttributes){
        jsonpCallback = ((ServletRequestAttributes)reqAttrs).getRequest().getParameter("jsonpCallback");
    }

    if(jsonpCallback != null){
        new PrintStream(outputMessage.getBody()).print(jsonpCallback + "(");
    }

    super.writeInternal(o, outputMessage);

    if(jsonpCallback != null){
        new PrintStream(outputMessage.getBody()).println(");");
    }
    }
}

如果请求的参数中带有jsonpCallback,那么会返回jsonp格式数据。比如:http://open.buy.qq.com/meta/api/1.xhtml?jsonpCallback=clientFunction。会返回clientFunction(…);

5. @CookieValue

    @CookieValue用于将请求的Cookie数据映射到功能处理方法的参数上。

public String test(@CookieValue(value="JSESSIONID", defaultValue="") String sessionId){
    ...
}

如上配置将自动将JSESSIONID值入参到sessionId参数上,defaultValue表示Cookie中没有JSESSIONID时默认为空。传入参数类型也可以是javax.servlet.http.Cookie类型。

public String test2(@CookieValue(value="JSESSIONID", defaultValue="") Cookie sessionId){
    ...
}

注意: 如果是使用cookies值来保持回话状态的话,推荐使用Spring的Bean Scopes机制,具体参见笔者的另一篇文章:Spring的Bean
Scopes
。非常方便。

6. @RequestHeader

    @RequestHeader用于将请求的头信息区数据映射到功能处理方法的参数上。

@RequestMapping(value="/header")
public String test(
   @RequestHeader("User-Agent") String userAgent,
   @RequestHeader(value="Accept") String[] accepts)

如上配置将自动将请求头“User-Agent”值入参到userAgent参数上,并将“Accept”请求头值入参到accepts参数上。

7. 返回多种表现形式(Returning multiple representations)

对于Restful服务,一个资源往往有多种表现形式,比如最常见的就是返回xml和json格式数据,还有就是RSS和ATOM。怎样让客户端告诉Restful服务,我希望得到什么样表现形式的资源呢?

一般来说client可以通过以下三者方式来通知Server它希望拿到的资源格式:

  1. 使用不同URI来表示同个资源的不同表现形式。一般使用不同的文件拓展名。如http://blog.arganzheng.me/users/argan.xml表示返回xml格式数据,而http://blog.arganzheng.me/users/aganzheng.json表示返回json格式.
  2. 使用一个请求参数告诉服务器希望得到的资源格式。如format=json。
  3. 使用同个URI,但是通过Accept HTTP request header来告诉server它理解的media types。例如同样请求http://blog.arganzheng.me/users/argan,如果带上text/xml accept header表示请求一个XML资源,带上application/pdf则表示期望收到pdf格式资源。

这其实就是Spring MVC默认的三个ContentNegotiationStrategy,即所谓的PPA Strategy(path extension, then parameter, then Accept header) ,顺序也是先path extension,然后parameter(默认是format参数),然后才是accept头。

Spring提供了ContentNegotiatingViewResolver来解决这个问题:

public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {

    private static final Log logger = LogFactory.getLog(ContentNegotiatingViewResolver.class);

    private static final String ACCEPT_HEADER = "Accept";

    private static final boolean jafPresent =
        ClassUtils.isPresent("javax.activation.FileTypeMap", ContentNegotiatingViewResolver.class.getClassLoader());

    private static final UrlPathHelper urlPathHelper = new UrlPathHelper();

    private int order = Ordered.HIGHEST_PRECEDENCE;

    private boolean favorPathExtension = true;

    private boolean favorParameter = false;

    private String parameterName = "format";

    private boolean useNotAcceptableStatusCode = false;

    private boolean ignoreAcceptHeader = false;

    private boolean useJaf = true;

    private ConcurrentMap<String, MediaType> mediaTypes = new ConcurrentHashMap<String, MediaType>();

    private List<View> defaultViews;

    private MediaType defaultContentType;

    private List<ViewResolver> viewResolvers;

    // ignore some setter and getter...    

    public void setMediaTypes(Map<String, String> mediaTypes) {
      Assert.notNull(mediaTypes, "'mediaTypes' must not be null");
      for (Map.Entry<String, String> entry : mediaTypes.entrySet()) {
        String extension = entry.getKey().toLowerCase(Locale.ENGLISH);
        MediaType mediaType = MediaType.parseMediaType(entry.getValue());
        this.mediaTypes.put(extension, mediaType);
      }
    }

    public void setDefaultViews(List<View> defaultViews) {
      this.defaultViews = defaultViews;
    }

    public void setDefaultContentType(MediaType defaultContentType) {
      this.defaultContentType = defaultContentType;
    }

    public void setViewResolvers(List<ViewResolver> viewResolvers) {
      this.viewResolvers = viewResolvers;
    }

    @Override
    protected void initServletContext(ServletContext servletContext) {
      if (this.viewResolvers == null) {
        Map<String, ViewResolver> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class);
        this.viewResolvers = new ArrayList<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 {
      RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
      Assert.isInstanceOf(ServletRequestAttributes.class, attrs);
      List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
      if (requestedMediaTypes != null) {
        List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
        View bestView = 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");
        return null;
      }
    }

    protected List<MediaType> getMediaTypes(HttpServletRequest request) {
      if (this.favorPathExtension) {
        String requestUri = urlPathHelper.getRequestUri(request);
        String filename = WebUtils.extractFullFilenameFromUrlPath(requestUri);
        MediaType mediaType = 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) {
          String parameterValue = request.getParameter(this.parameterName);
          MediaType mediaType = 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) {
        String acceptHeader = 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());
            }
            return null;
          }
        }
      }
      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) {
      String extension = StringUtils.getFilenameExtension(filename);
      if (!StringUtils.hasText(extension)) {
        return null;
      }
      extension = extension.toLowerCase(Locale.ENGLISH);
      MediaType mediaType = 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) {
      return this.mediaTypes.get(parameterValue.toLowerCase(Locale.ENGLISH));
    }

    private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
        throws Exception {

      List<View> candidateViews = new ArrayList<View>();
      for (ViewResolver viewResolver : this.viewResolvers) {
        View view = viewResolver.resolveViewName(viewName, locale);
        if (view != null) {
          candidateViews.add(view);
        }
        for (MediaType requestedMediaType : requestedMediaTypes) {
          List<String> extensions = getExtensionsForMediaType(requestedMediaType);
          for (String extension : extensions) {
            String viewNameWithExtension = 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 = new ArrayList<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) {
      MediaType bestRequestedMediaType = null;
      View bestView = null;
      for (MediaType requestedMediaType : requestedMediaTypes) {
        for (View candidateView : candidateViews) {
          if (StringUtils.hasText(candidateView.getContentType())) {
            MediaType candidateContentType = 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。所以关键在两点:

1.content negotiation策略 (ContentNegotiationStrategy)

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:

  1. 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.
  2. 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.
  3. 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.
  4. If the previous steps did not result in a media type, and ignoreAcceptHeader is false, the request Accept header is used.
  5. 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头。

2. 供选择的SingleViewResolver

  1. 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.)

    说明:即private List<ViewResolver> viewResolvers;属性。需要注意的是Spring会自动加载和注册所有其他的ViewResolver到ContentNegotiationViewResoloverviewResolvers属性。但是你需要告诉Spring MVC,你希望controller返回的view都是由ContentNegotiationViewResolover来解析,而不是其他定义的ViewResolver。这是通过order配置项来决定。你应该给ContentNegotiationViewResolover配置最高的order(其实默认就是最高了)。

  2. 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.

    说明:即private List<View> defaultViews;private MediaType defaultContentType;属性。

注意:@ResponseBody是为了单个View准备的,即它只能转换成一种格式,对于ContentNegotiatingViewResolver,需要多个SingleViewResolver来接收。

8. 客户端调用 Accessing RESTful services on the Client

Spring MVC不仅大大的简化了服务端RESTful服务的开发和开放,还提供了一些辅助类来方便客户端调用REST服务。

以前Client如果要调用REST服务,一般是使用HttpClient来发送HTTP请求:

String uri = "http://example.com/hotels/1/bookings";

PostMethod post = new PostMethod(uri);
String request = // create booking request content
post.setRequestEntity(new StringRequestEntity(request));

httpClient.executeMethod(post);

if (HttpStatus.SC_CREATED == post.getStatusCode()) {
  Header location = post.getRequestHeader("Location");
  if (location != null) {
    System.out.println("Created new booking at :" + location.getValue());
  }
}

太过底层,而且代码比较冗长,一般都要手动封装一下(即类似于SDK,封装了签名和HTTP发送和接受细节)。我们看一下Spring MVC是怎么解决这个问题的。 利用RestTemplate是client-site HTTP access的核心类。正如它的名称所示,RestTemplate非常类似于JdbcTemplate,JmsTemplate等XXXTemplate。这意味着RestTemplate是线程安全的并且可以通过callback来定制它的行为。Spring提供的Template类非常灵活和好用,种类也很丰富。当你需要做一些事情的时候可以先考虑一下有没有相应的template可以用。

RestTemplate默认使用java.net包下的基础类来创建HTTP请求。你可以实现ClientHttpRequestFactory接口,提供你自己的Http请求工厂类。Spring提供了CommonsClientHttpRequestFactory,这个工厂类使用Jakarta Commons HttpClient来创建HTTP请求。这样就可以使用HttpClient提供的认证和链接池功能了。

RestTemplate提供的方法如下

  • DELETE delete(String url, String… urlVariables)
  • GET getForObject(String url, Class responseType, String… urlVariables)
  • HEAD headForHeaders(String url, String… urlVariables)
  • OPTIONS optionsForAllow(String url, String… urlVariables)
  • POST postForLocation(String url, Object request, String… urlVariables)
  • PUT put(String url, Object request, String…urlVariables)
  • ANY exchange(String, HttpMethod, HttpEntity, Class, Object...) execute(String, HttpMethod, RequestCallback, ResponseExtractor, Object...)

方法名称很有规律,都是这个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.

其中getForObject()、postForLocation()和put()方法接收或者返回的参数通过HttpMessageConverter来转换为Http Request或者Http Response。这点与前面介绍服务端RESTful的@RequestBody@ResponseBody是一样的,Spring MVC默认会注册常用的Converter,你也可以自定义。

另外,每个方法的第一个参数都是一个url string,但是这个URI可以带有变量(还记得@PathVariable吗:)哦。参数有两种方式绑定值:

  1. 作为字符串变量数组(String variable arguments array)

     String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/bookings/{booking}", String.class, "42", "21");

    会转换为一个对http://example.com/hotels/42/bookings/21的GET请求。

  2. 或者Map对象(Map)

    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.

     Map<String, String> vars = new HashMap<String, String>();
     vars.put("hotel", "42");
     vars.put("booking", "21");
     String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/bookings/{booking}", String.class, vars);
    

     会转换为一个对`http://example.com/hotels/42/rooms/42`的GET请求。

9. 支持RESTful的URL

在开发功能模块之前,应该先把URL设计好。比查对 消息 这个资源的操作URL可以这么设计:

http://arganzheng.me/messages/show/123456
http://arganzheng.me/messages/preview/123456
http://arganzheng.me/messages/delete/123456
http://arganzheng.me/messages/new
http://arganzheng.me/message/update

说明:可以看到我们的URL中有动作在里面,事实上纯粹的RESTful URL是把动作隐含在HTTP头中:GET、PUT、DELETE、POST。不过这样对用户编码有要求,这个相对简单点。

要支持这种URL,web.xml需要这么配置:

<!-- REST servlet-mapping -->
<servlet-mapping>
    <servlet-name>DispatcherServlet<srvlet-name>
    <url-pattern>/</url-pattern>
<srvlet-mapping>

但是这样的话有个问题,就是静态文件也被mapping了,会导致找不到资源。Spring提供了一个resources配置项支持静态文件的处理16.14.5 Configuring
Serving of Resources

<!-- Forwards requests to the "/" resource to the "welcome" view -->
<mvc:view-controller path="/" view-name="index"/>

<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources/ directory -->
<mvc:resources mapping="/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 />

这样所有请求:http://arganzheng.me/resources/**会映射到webapp下的resources目录,而不是找我们的controller处理。

但是有个奇怪的问题,就是配置这个之后,原来动态东西就不能访问到了,提示找不到对应的handler,解决方案是增加一个<mvc:annotation-driven />配置。

另外,静态的html页面一般不放在resources路面下,而是直接在根目录下,比如:http://arganzheng.me/index.html或者http://arganzheng.me/404.html。所以应该在web.xml中在配置一个url-mapping规则:

<!-- 避免被Spring DispatcherServlet接管 -->
<servlet-mapping>
    <servlet-name>default<srvlet-name>
    <url-pattern>*.html</url-pattern>
<srvlet-mapping>
时间: 2024-10-19 08:30:00

SpringMVC之Restful的相关文章

springmvc的RESTful风格

springmvc对RESTful得支持RESTful架构,就是目前最流行得一种互联网软件架构.它结构清晰.符合标准.易于理解.扩展方便,所以挣得到越来越多网站的采用. RESTful(即Representational State Transfer变现层状态转换)其实是一个开发理念,是对http 的很好的诠释. 状态转换(State Transfer) 客户端用到的手段,只能是HTTP协议.具体来说就是HTTP协议里面四个表示操作方式的动词:GET/POST/PUT/DELETE,分别对应四中

maven+SpringMVC搭建RESTful后端服务框架

今天来尝试一下搭建基于SpringMVC的RESTful标准的后端服务. 首先,什么是MVC?M-model,模型,指业务数据层,具体一点就是业务逻辑与数据库的交互:V-view,视图,用来展示数据,传统SpringMVC中控制器有返回类型ModelAndView,即返回含有数据模型与页面视图的jsp文件:C-controller,控制器,通常负责处理与用户间的交互,控制从数据库取数与返回结果到用户等. 那么,什么是REST风格的服务呢?REST(Representational State T

SpringMVC实现Restful风格的WebService

1.环境 JDK7 MyEclipse2014 tomcat8 maven 3.3.3 spring4.1.4 2.创建maven工程 使用MyEclipse创建maven工程的方式可以参考这篇博文(链接1), 该博文中的第四小结是关于如何创建SpringMVC+maven教程.下面只给出创建好的目录结构,其中的部分文件如java程序是后面要完成的. 3.指定依赖文件 maven具有特定的文件结构,并通过pom.xml来管理工程.下面是在实现Restful时需要的依赖. 1 <project x

优雅的SpringMVC和Restful

一.前言 1.前段时间一直在写微信小程序的请求,终于把客户端的请求弄好了,可是服务端呢,该怎么写,纠结了半天,用servlet暂时写好了一个:http://www.cnblogs.com/JJDJJ/p/7299274.html 我们看一下整体代码: 当然这下面还有一大串.. 有没有发现,这些代码非常乱,以至于现在的我看着脑儿疼. 2.后来想着用API的那种形式写一个服务端,小程序这边post过去json格式数据,然后服务端获取json,接着对数据进行操作,最后返回结果就行了.类似百度外卖API

Springmvc构造RESTful详细讲解

/blog/1 HTTP GET => 得到id = 1的blog/blog/1 HTTP DELETE => 删除 id = 1的blog/blog/1 HTTP PUT => 更新id = 1的blog/blog HTTP POST => 新增BLOG 以下详细解一下spring rest使用. 首先,我们带着如下两个问题查看本文. 1.如何在java构造没有扩展名的RESTful url,如 /forms/1,而不是 /forms/1.do 2.浏览器的form标签不支持提交

SpringMVC构建Restful。

因为spring是依赖jackson来生成json,需要添加jar包. pom.xml文件添加依赖. <dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-mapper-asl</artifactId> <version>1.9.13</version> </dependency> <dependency>

SpringMVC&ndash;SSH -- RESTful -- JSR303

最近在使用SpringMVC+MyBatis感觉用起来效果很好.我不太明白SpringMVC和SSH的关系,遂搜索了一下.原来使用SpringMVC之后,可以替代之前的SSH这种开发模式. 附上知乎链接:https://www.zhihu.com/question/22014461 然后看到RESTful风格和JSR 303 之前指导RESTful风格,但是一直不知道此物为何物.今天研究了一下,附上两个链接: http://bbs.csdn.net/topics/390908212 http:/

SwaggerUI+SpringMVC——构建RestFul API的可视化界面

今天给大家介绍一款工具,这个工具目前可预见的好处是:自动维护最新的接口文档. 我们都知道,接口文档是非常重要的,但是随着代码的不断更新,文档却很难持续跟着更新,今天要介绍的工具,完美的解决了这个问题.而且,对于要使用我们接口的人来说,不需要在给他提供文档,告诉他地址,一目了然. 最近项目中一直有跟接口打交道,恰好又接触到了一个新的接口工具,拿出来跟大家分享一下. 关于REST接口,我在上篇文章中已经有介绍,这里来说一下如何配合SwaggerUI搭建RestFul API 的可视化界面.最终要达到

springMvc中restful风格的api路径中把小数点当参数,SpringMvc中url有小数点

在springMvc web项目中restful风格的api路径中有小数点会被过滤后台拿不到最后一个小数点的问题, 有两种解决方案: 1:在api路径中加入:.+ @RequestMapping("/findByIp/{ip:.+}") public Object test(@PathVariable String ip) { System.out.println(ip); return ""; } 但这种方式在web服务中感觉太过于鸡肋 所以在springMvc.