Spring MVC Annotated Controller Interface with @PathVariable

后端 未结 5 1594
走了就别回头了
走了就别回头了 2020-11-30 02:47

Is there any reason not to map Controllers as interfaces?

In all the examples and questions I see surrounding controllers, all are concrete classes. Is there a reaso

相关标签:
5条回答
  • 2020-11-30 03:38

    i resolved this problem.

    ON CLIENT SIDE:

    I'm using this library https://github.com/ggeorgovassilis/spring-rest-invoker/. This library generate a proxy from interface to invoke spring rest service.

    I extended this library:

    I created an annotations and a factory client class:

    Identify a Spring Rest Service

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface SpringRestService {
        String baseUri();
    }
    

    This class generates a client rest from interfaces

    public class RestFactory implements BeanFactoryPostProcessor,EmbeddedValueResolverAware  {
    
        StringValueResolver resolver;
    
        @Override
        public void setEmbeddedValueResolver(StringValueResolver resolver) {
            this.resolver = resolver;
        }
        private String basePackage = "com";
    
        public void setBasePackage(String basePackage) {
            this.basePackage = basePackage;
        }
    
    
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
            createBeanProxy(beanFactory,SpringRestService.class);
            createBeanProxy(beanFactory,JaxrsRestService.class);
        }
    
        private void createBeanProxy(ConfigurableListableBeanFactory beanFactory,Class<? extends Annotation> annotation) {
            List<Class<Object>> classes;
            try {
                classes = AnnotationUtils.findAnnotatedClasses(basePackage, annotation);
            } catch (Exception e) {
                throw new BeanInstantiationException(annotation, e.getMessage(), e);
            }
            BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
            for (Class<Object> classType : classes) {
                Annotation typeService = classType.getAnnotation(annotation);   
                GenericBeanDefinition beanDef = new GenericBeanDefinition();
                beanDef.setBeanClass(getQueryServiceFactory(classType, typeService));
                ConstructorArgumentValues cav = new ConstructorArgumentValues();
                cav.addIndexedArgumentValue(0, classType);
                cav.addIndexedArgumentValue(1, baseUri(classType,typeService));
                beanDef.setConstructorArgumentValues(cav);
                registry.registerBeanDefinition(classType.getName() + "Proxy", beanDef);
            }
        }
    
        private String baseUri(Class<Object> c,Annotation typeService){
            String baseUri = null;
            if(typeService instanceof SpringRestService){
                baseUri = ((SpringRestService)typeService).baseUri();  
            }else if(typeService instanceof JaxrsRestService){
                baseUri = ((JaxrsRestService)typeService).baseUri();
            }
            if(baseUri!=null && !baseUri.isEmpty()){
                return baseUri = resolver.resolveStringValue(baseUri);
            }else{
                throw new IllegalStateException("Impossibile individuare una baseUri per l'interface :"+c);
            }
        }
    
        private static Class<? extends FactoryBean<?>> getQueryServiceFactory(Class<Object> c,Annotation typeService){
            if(typeService instanceof SpringRestService){
                return it.eng.rete2i.springjsonmapper.spring.SpringRestInvokerProxyFactoryBean.class;  
            }else if(typeService instanceof JaxrsRestService){
                return it.eng.rete2i.springjsonmapper.jaxrs.JaxRsInvokerProxyFactoryBean.class;
            }
            throw new IllegalStateException("Impossibile individuare una classe per l'interface :"+c);
        }
    }
    

    I configure my factory:

    <bean class="it.eng.rete2i.springjsonmapper.factory.RestFactory">
        <property name="basePackage" value="it.giancarlo.rest.services" />
    </bean>
    

    ON REST SERVICE SIGNATURE

    this is an example interface:

    package it.giancarlo.rest.services.spring;
    
    import ...
    
    @SpringRestService(baseUri="${bookservice.url}")
    public interface BookService{
    
        @Override
        @RequestMapping("/volumes")
        QueryResult findBooksByTitle(@RequestParam("q") String q);
    
        @Override
        @RequestMapping("/volumes/{id}")
        Item findBookById(@PathVariable("id") String id);
    
    }
    

    ON REST SERVICE IMPLEMENTATION

    Service implementation

    @RestController
    @RequestMapping("bookService")
    public class BookServiceImpl implements BookService {
        @Override
        public QueryResult findBooksByTitle(String q) {
            // TODO Auto-generated method stub
            return null;
        }
        @Override
        public Item findBookById(String id) {
            // TODO Auto-generated method stub
            return null;
        }
    }
    

    To resolve annotation on parameters I create a custom RequestMappingHandlerMapping that looks all interfaces annotated with @SpringRestService

    public class RestServiceRequestMappingHandlerMapping extends RequestMappingHandlerMapping{
    
    
        public HandlerMethod testCreateHandlerMethod(Object handler, Method method){
            return createHandlerMethod(handler, method);
        }
    
        @Override
        protected HandlerMethod createHandlerMethod(Object handler, Method method) {
            HandlerMethod handlerMethod;
            if (handler instanceof String) {
                String beanName = (String) handler;
                handlerMethod = new RestServiceHandlerMethod(beanName,getApplicationContext().getAutowireCapableBeanFactory(), method);
            }
            else {
                handlerMethod = new RestServiceHandlerMethod(handler, method);
            }
            return handlerMethod;
        }
    
    
        public static class RestServiceHandlerMethod extends HandlerMethod{
    
            private Method interfaceMethod;
    
    
            public RestServiceHandlerMethod(Object bean, Method method) {
                super(bean,method);
                changeType();
            }
    
            public RestServiceHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
                super(bean,methodName,parameterTypes);
                changeType();
            }
    
            public RestServiceHandlerMethod(String beanName, BeanFactory beanFactory, Method method) {
                super(beanName,beanFactory,method);
                changeType();
            }
    
    
            private void changeType(){
                for(Class<?> clazz : getMethod().getDeclaringClass().getInterfaces()){
                    if(clazz.isAnnotationPresent(SpringRestService.class)){
                        try{
                            interfaceMethod = clazz.getMethod(getMethod().getName(), getMethod().getParameterTypes());
                            break;      
                        }catch(NoSuchMethodException e){
    
                        }
                    }
                }
                MethodParameter[] params = super.getMethodParameters();
                for(int i=0;i<params.length;i++){
                    params[i] = new RestServiceMethodParameter(params[i]);
                }
            }
    
    
    
    
            private class RestServiceMethodParameter extends MethodParameter{
    
                private volatile Annotation[] parameterAnnotations;
    
                public RestServiceMethodParameter(MethodParameter methodParameter){
                    super(methodParameter);
                }
    
    
                @Override
                public Annotation[] getParameterAnnotations() {
                    if (this.parameterAnnotations == null){
                            if(RestServiceHandlerMethod.this.interfaceMethod!=null) {
                                Annotation[][] annotationArray = RestServiceHandlerMethod.this.interfaceMethod.getParameterAnnotations();
                                if (this.getParameterIndex() >= 0 && this.getParameterIndex() < annotationArray.length) {
                                    this.parameterAnnotations = annotationArray[this.getParameterIndex()];
                                }
                                else {
                                    this.parameterAnnotations = new Annotation[0];
                                }
                            }else{
                                this.parameterAnnotations = super.getParameterAnnotations();
                            }
                    }
                    return this.parameterAnnotations;
                }
    
            }
    
        }
    
    }
    

    I created a configuration class

    @Configuration
    public class WebConfig extends WebMvcConfigurationSupport{
    
        @Bean
        public RequestMappingHandlerMapping requestMappingHandlerMapping() {
            RestServiceRequestMappingHandlerMapping handlerMapping = new RestServiceRequestMappingHandlerMapping();
            handlerMapping.setOrder(0);
            handlerMapping.setInterceptors(getInterceptors());
            handlerMapping.setContentNegotiationManager(mvcContentNegotiationManager());
    
            PathMatchConfigurer configurer = getPathMatchConfigurer();
            if (configurer.isUseSuffixPatternMatch() != null) {
                handlerMapping.setUseSuffixPatternMatch(configurer.isUseSuffixPatternMatch());
            }
            if (configurer.isUseRegisteredSuffixPatternMatch() != null) {
                handlerMapping.setUseRegisteredSuffixPatternMatch(configurer.isUseRegisteredSuffixPatternMatch());
            }
            if (configurer.isUseTrailingSlashMatch() != null) {
                handlerMapping.setUseTrailingSlashMatch(configurer.isUseTrailingSlashMatch());
            }
            if (configurer.getPathMatcher() != null) {
                handlerMapping.setPathMatcher(configurer.getPathMatcher());
            }
            if (configurer.getUrlPathHelper() != null) {
                handlerMapping.setUrlPathHelper(configurer.getUrlPathHelper());
            }
            return handlerMapping;
        }
    }
    

    and I configurated it

    <bean class="....WebConfig" />
    
    0 讨论(0)
  • 2020-11-30 03:42

    Apparently, when a request pattern is mapped to a method via the @RequestMapping annotation, it is mapped to to the concrete method implementation. So a request that matches the declaration will invoke GoalServiceImpl.removeGoal() directly rather than the method that originally declared the @RequestMapping ie GoalService.removeGoal().

    Since an annotation on an interface, interface method, or interface method parameter does not carry over to the implementation there is no way for Spring MVC to recognize this as a @PathVariable unless the implementing class declares it explicitly. Without it, any AOP advice that targets @PathVariable parameters will not be executed.

    0 讨论(0)
  • 2020-11-30 03:45

    Recently I had the same problem. Following has worked for me:

    public class GoalServiceImpl implements GoalService {
        ...
        public void removeGoal(@PathVariableString id) {
        }
    }
    
    0 讨论(0)
  • 2020-11-30 03:47

    It works in newer version of Spring.

    import org.springframework.web.bind.annotation.RequestMapping;
    public interface TestApi {
        @RequestMapping("/test")
        public String test();
    }
    

    Implement the interface in the Controller

    @RestController
    @Slf4j
    public class TestApiController implements TestApi {
    
        @Override
        public String test() {
            log.info("In Test");
            return "Value";
        }
    
    }
    

    It can be used as: Rest client

    0 讨论(0)
  • 2020-11-30 03:48

    The feature of defining all bindings on interface actually got implement recently in Spring 5.1.5.

    Please see this issue: https://github.com/spring-projects/spring-framework/issues/15682 - it was a struggle :)

    Now you can actually do:

    @RequestMapping("/random")
    public interface RandomDataController {
    
        @RequestMapping(value = "/{type}", method = RequestMethod.GET)
        @ResponseBody
        RandomData getRandomData(
                @PathVariable(value = "type") RandomDataType type, @RequestParam(value = "size", required = false, defaultValue = "10") int size);
    }
    
    @Controller
    public class RandomDataImpl implements RandomDataController {
    
        @Autowired
        private RandomGenerator randomGenerator;
    
        @Override
        public RandomData getPathParamRandomData(RandomDataType type, int size) {
            return randomGenerator.generateRandomData(type, size);
        }
    }
    

    You can even use this library: https://github.com/ggeorgovassilis/spring-rest-invoker

    To get a client-proxy based on that interface, similarly to how RestEasys client framework works in the JAX-RS land.

    0 讨论(0)
提交回复
热议问题