субота, 15 січня 2011 р.

REST with Spring: Strict resource URLs

Russian translation is posted on my blog at Habrahabr

As you may know Spring MVC is using new annotation driven configuration model since 2.5. To enable it, you should use the <mvc:annotation-driven /> tag in your Spring configuration file..

What it does under the hood is it registers DefaultAnnotationHandlerMapping and AnnotationMethodHandlerAdaptor in your application context 

What DefaultAnnotationHandlerMapping does is searches your classes for @RequestMapping annotation and creates mapping for each and on top of that two mappings ending with .* and /.

So once you have following controller:

@Controller
@RequestMapping("/service/hotels")
public class HotelsCollectionController {

    @Autowired
    private HotelService hotelService;

    @RequestMapping(method = RequestMethod.GET)
    public String getHotelList(Model model) {

        List<Hotel> list = hotelService .getHotelList();

        model.addAttribute("hotels", list);

        return "service/hotels/read";
    }

    public void setHotelService(HotelService hotelService) {
        this.hotelService = hotelService;
    }
}

you will get three mappings internally for /service/hotels, /service/hotels/ and /service/hotels.*

The purpose of first two is clearly to be more friendly to the user and the last is used for content negotiation with ContentNegotiatingViewResolver.

And thats fine for most web applications. 

The problem arises when you try to apply RESTful approach to your web services using annotations for mapping. Since URL is now effectively a resource, different URLs are now different resources and your application should not be vague about interpreting it.

Another point is that Spring itself treats these implied URLs without too much care by default.

Naive me configured DefaultAnnotationHandlerMapping for application and set its defaultSuffixPattern property to false. And if that would be as simple as that, there was no much sense in this post.

Straightforward solves the problem at first site 

    <mvc:annotation-driven />

    <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
        <property name="useDefaultSuffixPattern" value="false" />
    </bean>

Now think about it. Once Spring sees <mvc:annotation-driven /> it creates one DefaultAnnotationHandlerMapping and places it in the context. Once it sees the above bean definition it creates *another* instance of DefaultAnnotationHandlerMapping and also places it in the context. So your happy app will have two HandlerMappings, one with default settings and another  configured one. Now which HandlerMapper will happen to chew the HTTP request first depends on the internal order of two, and is completely out of your control (well, almost... you can apply ordering, but that's kinda hack and besides trashes the story).

While for /service/hotels there is no difference, for /service/hotels/ and /service/hotels.* there is. Chances are that you will be using ContentNegotiatingViewResolver to negotiate best representation of resource for client and in this case you've effectively lost control on resolving the correct View, resulting from incorrect view being returned and ending with 500 on server. I hope I'll find time to gather the details later.

To avoid this, you should remove one of HandlerMappings from context. So we should remove <mvc:annotation-driven /> completely and do the hard work of registering AnnotationMethodHandlerAdaptor.

    <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
        <property name="useDefaultSuffixPattern" value="false" />
    </bean>

    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />

This should do what is required and you will get desired 404 on implicit URLs. 

Немає коментарів:

Дописати коментар