Spring MVC带有@PathVariable的注释控制器接口

44

是否有任何不将控制器映射为接口的原因?

在所有与控制器相关的示例和问题中,所有控制器都是具体类。这是有原因的吗?我想将请求映射与实现分离。但当我尝试在我的具体类中将@PathVariable作为参数时,我遇到了难题。

我的控制器接口如下所示:

@Controller
@RequestMapping("/services/goal/")
public interface GoalService {

    @RequestMapping("options/")
    @ResponseBody
    Map<String, Long> getGoals();

    @RequestMapping(value = "{id}/", method = RequestMethod.DELETE)
    @ResponseBody
    void removeGoal(@PathVariable String id);

}

实现类:

@Component
public class GoalServiceImpl implements GoalService {

    /* init code */

    public Map<String, Long> getGoals() {
        /* method code */
        return map;
    }

    public void removeGoal(String id) {
        Goal goal = goalDao.findByPrimaryKey(Long.parseLong(id));
        goalDao.remove(goal);
    }

}

getGoals()方法效果很好;removeGoal(String id)会抛出异常。

ExceptionHandlerExceptionResolver - Resolving exception from handler [public void
    todo.webapp.controllers.services.GoalServiceImpl.removeGoal(java.lang.String)]: 
    org.springframework.web.bind.MissingServletRequestParameterException: Required 
    String parameter 'id' is not present
如果我在具体的类中添加@PathVariable注释,那么一切都按预期进行,但是为什么我必须在具体的类中重新声明这个注释呢?难道不应该由带有@Controller注释的内容处理吗?

2
看起来我并没有理解注释继承,等8小时超时结束后,我会发布我的解释。 - willscripted
5个回答

29

显然,当通过@RequestMapping注释将请求模式映射到方法时,它会被映射到具体的方法实现。 因此,与声明匹配的请求将直接调用GoalServiceImpl.removeGoal(),而不是最初声明@RequestMapping的方法,即GoalService.removeGoal()

由于在接口、接口方法或接口方法参数上的注释不会传递到实现中,因此Spring MVC无法将其识别为@PathVariable,除非实现类显式声明该注释。如果没有它,则任何针对@PathVariable参数的AOP建议将不会被执行。


1
要明确的是,Spring MVC 没有理由不能做到这一点。在 Jersey 中它可以很好地工作。但出于某种原因,他们选择不这样做。 - Michael Haefele
2
最近增加了对接口注释的全面支持 - 请查看下面的答案并点赞。 - Adam from WALCZAK.IT

17

最近Spring 5.1.5实现了在接口上定义所有绑定的功能。

请参阅此问题:https://github.com/spring-projects/spring-framework/issues/15682 - 它曾是一场斗争 :)

现在,您可以这样做:

@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);
    }
}

您甚至可以使用这个库:https://github.com/ggeorgovassilis/spring-rest-invoker

要获取基于该接口的客户端代理,类似于RestEasy在JAX-RS领域的客户端框架工作方式。


10

它在新版本的Spring中有效。

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

在Controller中实现接口

@RestController
@Slf4j
public class TestApiController implements TestApi {

    @Override
    public String test() {
        log.info("In Test");
        return "Value";
    }

}

它可以用作: REST客户端


2
你用带标注的参数测试过吗? - willscripted
@Sabuh Das,你使用的是哪个版本? - kozla13
3
Spring 4.3.* 可以工作,但是这个例子中没有参数。Spring MVC 处理器适配器并不知道接口方法中的任何注释参数。有一些解决方法:带有 Spring MVC 注解的接口方法可以有默认实现。该实现将调用另一个由带有@RestController注解的类实现的“抽象”方法。在这种情况下,Spring 将请求绑定到具有默认实现的接口方法。请查看示例 - Timur Milovanov
1
@TimurMilovanov,您提供的简单示例似乎包含错误。控制器覆盖了默认接口方法sayHello而不是sayHelloImpl。我认为这段代码甚至无法编译。但我尝试了您的意思,解决方法很好用 ;) - Gerard Bosch
@TimurMilovanov,你的评论是一个非常完整的答案。你可以把它作为一个答案发布,因为它非常有价值,并且提供了一个可行的解决方案示例,适用于Spring 4.3之前的情况;) - Gerard Bosch
显示剩余2条评论

1

最近我遇到了同样的问题。以下方法对我有效:

public class GoalServiceImpl implements GoalService {
    ...
    public void removeGoal(@PathVariableString id) {
    }
}

0

我解决了这个问题。

客户端:

我正在使用这个库https://github.com/ggeorgovassilis/spring-rest-invoker/。这个库可以从接口生成代理来调用Spring Rest服务。

我扩展了这个库:

我创建了一个注释和一个工厂客户端类:

识别Spring Rest服务

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

这个类从接口生成客户端rest

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);
    }
}

我配置我的工厂:

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

关于REST服务签名

这是一个示例接口:

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);

}

关于REST服务实现

服务实现

@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;
    }
}

为了解决参数注释问题,我创建了一个自定义的RequestMappingHandlerMapping,它查找所有使用@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;
            }

        }

    }

}

我创建了一个配置类

@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;
    }
}

然后我进行了配置

<bean class="....WebConfig" />

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接