¿Hay alguna razón para no asignar controladores como interfaces?
En todos los ejemplos y preguntas veo los controladores de los alrededores, todos son clases concretas. ¿Hay alguna razón para esto? Me gustaría separar las asignaciones de solicitudes de la implementación. Sin embargo, @PathVariable
contra una pared cuando traté de obtener @PathVariable
como parámetro en mi clase concreta.
La interfaz de mi controlador se ve así:
@Controller @RequestMapping("/services/goal/") public interface GoalService { @RequestMapping("options/") @ResponseBody Map getGoals(); @RequestMapping(value = "{id}/", method = RequestMethod.DELETE) @ResponseBody void removeGoal(@PathVariable String id); }
Y la clase de implementación:
@Component public class GoalServiceImpl implements GoalService { /* init code */ public Map getGoals() { /* method code */ return map; } public void removeGoal(String id) { Goal goal = goalDao.findByPrimaryKey(Long.parseLong(id)); goalDao.remove(goal); } }
El método getGoals()
funciona de maravilla; el removeGoal(String id)
arroja una excepción
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
Si agrego la anotación @PathVariable
a la clase concreta todo funciona como se esperaba, pero ¿por qué tendré que volver a declarar esto en la clase concreta? ¿No debería ser manejado por lo que tenga la anotación @Controller
?
Aparentemente, cuando un patrón de solicitud se asigna a un método a través de la anotación @RequestMapping
, se asigna a la implementación del método concreto. Por lo tanto, una solicitud que coincida con la statement invocará directamente a GoalServiceImpl.removeGoal()
lugar del método que originalmente declaró @RequestMapping
ie GoalService.removeGoal()
.
Como una anotación en una interfaz, método de interfaz o parámetro de método de interfaz no se @PathVariable
a la implementación, Spring MVC no puede reconocer esto como @PathVariable
menos que la clase implementadora lo declare explícitamente. Sin él, ningún consejo de AOP que se @PathVariable
parámetros de @PathVariable
no se ejecutará.
Funciona en una versión más nueva de Spring.
import org.springframework.web.bind.annotation.RequestMapping; public interface TestApi { @RequestMapping("/test") public String test(); }
Implementar la interfaz en el controlador
@RestController @Slf4j public class TestApiController implements TestApi { @Override public String test() { log.info("In Test"); return "Value"; } }
Se puede usar como: Cliente de reposo
Resolví este problema.
EN EL LADO DEL CLIENTE:
Estoy usando esta biblioteca https://github.com/ggeorgovassilis/spring-rest-invoker/ . Esta biblioteca genera un proxy de la interfaz para invocar el servicio de descanso de spring.
Extendí esta biblioteca:
Creé una anotación y una clase de cliente de fábrica:
Identificar un servicio de descanso de spring
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SpringRestService { String baseUri(); }
Esta clase genera un descanso del cliente de las 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> classes; try { classes = AnnotationUtils.findAnnotatedClasses(basePackage, annotation); } catch (Exception e) { throw new BeanInstantiationException(annotation, e.getMessage(), e); } BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; for (Class
Configuro mi fábrica:
FIRMA DEL SERVICIO DE RESTO
esta es una interfaz de ejemplo:
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); }
EN LA IMPLEMENTACIÓN DEL SERVICIO DE RESTO
Implementación del servicio
@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; } }
Para resolver la anotación en los parámetros, creo un RequestMappingHandlerMapping personalizado que busca todas las interfaces anotadas con @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= 0 && this.getParameterIndex() < annotationArray.length) { this.parameterAnnotations = annotationArray[this.getParameterIndex()]; } else { this.parameterAnnotations = new Annotation[0]; } }else{ this.parameterAnnotations = super.getParameterAnnotations(); } } return this.parameterAnnotations; } } } }
Creé una clase de configuración
@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; } }
y lo configuré