Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does one map spring-webmvc paths dynamically?

This is a cross post. I've also posted the same question to spring forums. http://forum.springsource.org/showthread.php?128579-Database-driven-Controller-Mapping

Hi I'm trying to do database driven controller mappings so that they can change at runtime.

So far what I have is as follows.

Custom Handler Adaptor which can always be optimized later.

@Component
public class DatabasePageUrlHandlerMapping extends AbstractUrlHandlerMapping implements PriorityOrdered {


    @Override
    protected Object getHandlerInternal(HttpServletRequest request)
            throws Exception {
        String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
        List<Page> pages = Page.findAllPages();
        for (Page page : pages) {
            if (lookupPath.equals(page.getSeoPath())) {
                Object handler = getApplicationContext().getBean("_pageViewController");
                return new HandlerExecutionChain(handler);
            }
        }
        return super.getHandlerInternal(request);
    }

}

my webmvc-config looks as follows (the relevant part)

Code:

<context:component-scan base-package="com.artiststogether"
    use-default-filters="false">
    <context:include-filter expression="org.springframework.stereotype.Controller"
        type="annotation" />
</context:component-scan>

<!-- If I don't put an order into this it doesn't fail over to the implementation why? -->
<bean class="com.artiststogether.web.DatabasePageUrlHandlerMapping" p:order="-1" />
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>

This appears to be picking up the correct controller. However I recieve an error when going to a database defined path (such as "/a")

java.lang.NullPointerException
    at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter$ServletHandlerMethodResolver.useTypeLevelMapping(AnnotationMethodHandlerAdapter.java:675)
    at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter$ServletHandlerMethodResolver.resolveHandlerMethod(AnnotationMethodHandlerAdapter.java:585)
    at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.invokeHandlerMethod(AnnotationMethodHandlerAdapter.java:431)
    at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.handle(AnnotationMethodHandlerAdapter.java:424)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:900)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:827)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:882)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:778)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:621)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:722)
        ....

Do I need to define a custom annotation handler?

To be honest this whole process seems more difficult than it should. I want 1 controller to handle all requests to an externally defined url path is this the correct way of going arround it.

I'd also like to pass in the object which matched into the controller if this is possible rather than doing a fresh lookup in the controller. This will basically form my model for the view.

Any advise on how to get this working?

EDIT For the record the NPE is here

    private boolean useTypeLevelMapping(HttpServletRequest request) {
        if (!hasTypeLevelMapping() || ObjectUtils.isEmpty(getTypeLevelMapping().value())) {
            return false;
        }
        return (Boolean) request.getAttribute(HandlerMapping.INTROSPECT_TYPE_LEVEL_MAPPING);
    }

Another Edit version numbers from the pom.xml

<properties>
    <aspectj.version>1.6.12</aspectj.version>
    <java.version>6</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <roo.version>1.2.1.RELEASE</roo.version>
    <slf4j.version>1.6.4</slf4j.version>
    <spring.version>3.1.0.RELEASE</spring.version>
<spring-security.version>3.1.0.RELEASE</spring-security.version>
</properties>

I've answered the question myself below but I'm still intrested in people weighing in on the correct way to do this.

like image 964
Wes Avatar asked Jul 20 '12 14:07

Wes


People also ask

How do you find the path variable in interceptor?

Add a Interceptor. Register the Interceptor. With this you will be able to read the @PathVariable by the name you have given to the pathvariable for all the request made.

What is spring Webmvc?

A Spring MVC is a Java framework which is used to build web applications. It follows the Model-View-Controller design pattern. It implements all the basic features of a core spring framework like Inversion of Control, Dependency Injection.

How does controller work in spring?

The @Controller annotation indicates that a particular class serves the role of a controller. Spring Controller annotation is typically used in combination with annotated handler methods based on the @RequestMapping annotation. It can be applied to classes only. It's used to mark a class as a web request handler.


1 Answers

Apparently from the lack of answers to the contrary here and on the spring forums it appears that there is no simpler way to do this within the spring framework.

I have however managed to get it working and I've shared a project at github that can be built with maven that add 4 classes to ease with the process of dynamically adding class. This project can be found at https://github.com/Athas1980/MvcBackingBean. I'll also share another project to prove that it works.

Thanks to Marten Deinum, and Rossen Stoyanchev


For those interested in how to achieve this yourselves you need to do the following

  1. Implement an instance of HandlerMapper This gives you the mapping between a controller class and the url that you are mapping to.

    //   Copyright 2012 Wesley Acheson
    //
    //   Licensed under the Apache License, Version 2.0 (the "License");
    //   you may not use this file except in compliance with the License.
    //   You may obtain a copy of the License at
    //
    //       http://www.apache.org/licenses/LICENSE-2.0
    //
    //   Unless required by applicable law or agreed to in writing, software
    //   distributed under the License is distributed on an "AS IS" BASIS,
    //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    //   See the License for the specific language governing permissions and
    //   limitations under the License.
    
    package com.wesley_acheson.spring;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.core.PriorityOrdered;
    import org.springframework.web.servlet.HandlerExecutionChain;
    import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    
    /**
     * A Handler mapper that delegates to a {@link UrlBackingBeanMapper} to know
     * whether it should match a url. If it does match a url then it adds the bean
     * which matches the url to the request.
     * 
     * @author Wesley Acheson
     * 
     */
    public class BackingBeanUrlHandlerMapper extends AbstractUrlHandlerMapping
            implements PriorityOrdered {
    
        private UrlBackingBeanMapper<?> urlMapper;
    
        /**
         * 
         * @param urlMapper
         *            The bean which matches urls with other beans.
         */
        public void setUrlMapper(UrlBackingBeanMapper<?> urlMapper) {
            this.urlMapper = urlMapper;
        }
    
        protected UrlBackingBeanMapper<?> getUrlMapper() {
            return urlMapper;
        }
    
        public static final String BACKING_BEAN_ATTRIBUTE = BackingBeanUrlHandlerMapper.class
                .getName() + ".backingBean";
    
        /**
         * The controller which control will be passed to if there is any beans
         * matching in @{link {@link #setUrlMapper(UrlBackingBeanMapper)}.
         */
        public Object controller;
    
        /**
         * @param controller
         *            <p>
         *            The controller which control will be passed to if there is any
         *            beans matching in @{link
         *            {@link #setUrlMapper(UrlBackingBeanMapper)}.
         */
        public void setController(Object controller) {
            this.controller = controller;
        }
    
        /*
         * (non-Javadoc)
         * 
         * @see org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#
         * lookupHandler(java.lang.String, javax.servlet.http.HttpServletRequest)
         */
        @Override
        protected Object lookupHandler(String urlPath, HttpServletRequest request)
                throws Exception {
    
            if (urlMapper.isPathMapped(urlPath)) {
                Object bean = urlMapper.retrieveBackingBean(urlPath);
                return buildChain(bean, urlPath);
            }
    
            return super.lookupHandler(urlPath, request);
        }
    
        /**
         * Builds a handler execution chain that contains both a path exposing
         * handler and a backing bean exposing handler.
         * 
         * @param bean
         *            The object to be wrapped in the handler execution chain.
         * @param urlPath
         *            The path which matched. In this case the full path.
         * @return The handler execution chain that contains the backing bean.
         * 
         * @see {@link AbstractUrlHandlerMapping#buildPathExposingHandler(Object, String, String, java.util.Map)}
         *    
         */
        protected HandlerExecutionChain buildChain(Object bean, String urlPath) {
            // I don't know why but the super class declares object but actually
            // returns handlerExecution chain.
            HandlerExecutionChain chain = (HandlerExecutionChain) buildPathExposingHandler(
                    controller, urlPath, urlPath, null);
            addBackingBeanInteceptor(chain, bean);
            return chain;
        }
    
        /**
         * Adds an inteceptor which adds the backing bean into the request to an
         * existing HandlerExecutionChain.
         * 
         * @param chain
         *            The chain which the backing bean is being added to.
         * @param bean
         *            The object to pass through to the controller.
         */
        protected void addBackingBeanInteceptor(HandlerExecutionChain chain,
                Object bean) {
            chain.addInterceptor(new BackingBeanExposingInteceptor(bean));
    
        }
    
        /**
         * An Interceptor which adds a bean to a request for later consumption by a
         * controller.
         * 
         * @author Wesley Acheson
         * 
         */
        protected class BackingBeanExposingInteceptor extends
                HandlerInterceptorAdapter {
            private Object backingBean;
    
            /**
             * @param backingBean
             *            the bean which is passed through to the controller.
             */
            public BackingBeanExposingInteceptor(Object backingBean) {
                this.backingBean = backingBean;
            }
    
            @Override
            public boolean preHandle(HttpServletRequest request,
                    HttpServletResponse response, Object handler) throws Exception {
                request.setAttribute(BACKING_BEAN_ATTRIBUTE, backingBean);
                return true;
            }
        }
    
    }
    
  2. Implement a HandlerMethodArgumentResolver to fetch the value out of the session. (assuming you are intrested in setting in the session)

    //   Copyright 2012 Wesley Acheson
    //
    //   Licensed under the Apache License, Version 2.0 (the "License");
    //   you may not use this file except in compliance with the License.
    //   You may obtain a copy of the License at
    //
    //       http://www.apache.org/licenses/LICENSE-2.0
    //
    //   Unless required by applicable law or agreed to in writing, software
    //   distributed under the License is distributed on an "AS IS" BASIS,
    //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    //   See the License for the specific language governing permissions and
    //   limitations under the License.
    
    package com.wesley_acheson.spring;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.springframework.core.MethodParameter;
    import org.springframework.web.bind.support.WebDataBinderFactory;
    import org.springframework.web.context.request.NativeWebRequest;
    import org.springframework.web.method.support.HandlerMethodArgumentResolver;
    import org.springframework.web.method.support.ModelAndViewContainer;
    
    /**
     * Resolves method parameters which are annotated with {@link BackingBean}.
     * 
     * <b>Note:</b> Only works for Http requests.
     * 
     * @author Wesley Acheson
     * 
     */
    public class BackingBeanValueResolver implements HandlerMethodArgumentResolver {
    
        /**
         * Constructor.
         */
        public BackingBeanValueResolver() {
        }
    
        /**
         * Implementation of
         * {@link HandlerMethodArgumentResolver#supportsParameter(MethodParameter)}
         * that returns true if the method parameter is annotatated with
         * {@link BackingBean}.
         */
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            return parameter.hasParameterAnnotation(BackingBean.class);
        }
    
        @Override
        public Object resolveArgument(MethodParameter parameter,
                ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
                WebDataBinderFactory binderFactory) throws Exception {
            return webRequest.getNativeRequest(HttpServletRequest.class)
                    .getAttribute(
                            BackingBeanUrlHandlerMapper.BACKING_BEAN_ATTRIBUTE);
        }
    
    }
    
  3. Implement a custom WebArgumentResolver to fetch the instance of the Bean passed. Set this as a property to an instance of AnnotationMethodHandler.

    /**
     * 
     */
    package com.wesley_acheson.spring;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.springframework.core.MethodParameter;
    import org.springframework.web.bind.support.WebArgumentResolver;
    import org.springframework.web.context.request.NativeWebRequest;
    
    
    /**
     * @author Wesley Acheson
     *
     */
    public class BackingBeanArgumentResolver implements WebArgumentResolver {
    
        /* (non-Javadoc)
         * @see org.springframework.web.bind.support.WebArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.context.request.NativeWebRequest)
         */
        @Override
        public Object resolveArgument(MethodParameter methodParameter,
                NativeWebRequest webRequest) throws Exception {
            if (methodParameter.hasParameterAnnotation(BackingBean.class))
            {
                HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
                Object parameter = request.getAttribute(BackingBeanUrlHandlerMapper.BACKING_BEAN_ATTRIBUTE);
                if (parameter == null)
                {
                    return UNRESOLVED;
                }
                if (methodParameter.getParameterType().isAssignableFrom(parameter.getClass()))
                {
                    return parameter;
                }
            }
    
    
            return UNRESOLVED;
        }
    
    }
    
  4. I also created a BackingBean annotation and an interface to pass to my handler addapters as I felt they were easier.

  5. Create your controller. If you use my code you will want to inject the argument using the @BackingBean annotation. The request mapping on the controller itself must not match any good urls (This is because we bypass this step with our handler adapter and we don't want the default annotation handler to pick it up.

  6. Wire up everything in spring. Here's an example file from my working example project.

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
        xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-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/util http://www.springframework.org/schema/util/spring-util-3.1.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
    
        <!-- The controllers are autodetected POJOs labeled with the @Controller 
            annotation. -->
        <context:component-scan base-package="com.wesley_acheson"
            use-default-filters="false">
            <context:include-filter expression="org.springframework.stereotype.Controller"
                type="annotation" />
        </context:component-scan>
    
        <bean class="com.wesley_acheson.spring.BackingBeanUrlHandlerMapper"
            p:order="-1">
            <property name="controller">
                <!-- A simple example controller. -->
                <bean class="com.wesley_acheson.example.PageController" />
            </property>
            <!--  A simple example mapper. -->
            <property name="urlMapper">
                <bean class="com.wesley_acheson.example.PageBeanUrlMapper" />
            </property>
        </bean>
    
        <util:map id="pages">
            <entry key="/testPage1">
                <bean class="com.wesley_acheson.example.Page">
                    <property name="title" value="Test Page 1 title" />
                    <property name="contents"
                        value="This is the first test page.&lt;br /&gt; It's only purpose is to check
                        if &lt;b&gt;BackingBeans&lt;/b&gt; work." />
                </bean>
            </entry>
    
            <entry key="/test/nested">
                <bean class="com.wesley_acheson.example.Page">
                    <property name="title" value="Nested Path" />
                    <property name="contents"
                        value="This is another test page its purpose is to ensure nested pages work." />
                </bean>
            </entry>
        </util:map>
    
    
        <bean
            class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
            <property name="customArgumentResolver">
                <bean class="com.wesley_acheson.spring.BackingBeanArgumentResolver" />
            </property>
        </bean>
    
        <!-- Turns on support for mapping requests to Spring MVC @Controller methods 
            Also registers default Formatters and Validators for use across all @Controllers -->
        <mvc:annotation-driven />
    
    
        <!-- Handles HTTP GET requests for /resources/** by efficiently serving 
            up static resources -->
        <mvc:resources location="/, classpath:/META-INF/web-resources/"
            mapping="/resources/**" />
    
        <!-- Allows for mapping the DispatcherServlet to "/" by forwarding static 
            resource requests to the container's default Servlet -->
        <mvc:default-servlet-handler />
    
    </beans>
    
like image 178
Wes Avatar answered Nov 09 '22 22:11

Wes