Y a-t-il une raison de ne pas mapper les contrôleurs en tant qu'interfaces?
Dans tous les exemples et questions que je vois autour des contrôleurs, tous sont des classes concrètes. Y a-t-il une raison à cela? Je souhaite séparer les mappages de demandes de la mise en œuvre. J'ai heurté un mur quand j'ai essayé d'obtenir un @PathVariable
en tant que paramètre de ma classe concrète.
Mon interface de contrôleur ressemble à ceci:
@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);
}
Et la classe d'implémentation:
@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);
}
}
La méthode getGoals()
fonctionne très bien; removeGoal(String id)
lève une exception
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 j'ajoute l'annotation @PathVariable
à la classe concrète, tout fonctionne comme prévu, mais pourquoi devrais-je être obligé de re-déclarer ceci dans la classe concrète? Ne devrait-il pas être traité par ce qui a l'annotation @Controller
?
Apparemment, lorsqu'un modèle de demande est mappé sur une méthode via l'annotation @RequestMapping
, il est mappé sur l'implémentation de méthode concrète. Ainsi, une demande qui correspond à la déclaration appellera directement GoalServiceImpl.removeGoal()
plutôt que la méthode qui a déclaré à l'origine le @RequestMapping
c'est-à-dire GoalService.removeGoal()
.
Dans la mesure où une annotation sur une interface, une méthode d'interface ou un paramètre de méthode interface ne se répercute pas sur l'implémentation, Spring MVC ne peut la reconnaître en tant que @PathVariable
que si la classe d'implémentation le déclare explicitement. Sans lui, aucun conseil AOP ciblant les paramètres @PathVariable
ne sera exécuté.
Cela fonctionne dans la nouvelle version de Spring.
import org.springframework.web.bind.annotation.RequestMapping;
public interface TestApi {
@RequestMapping("/test")
public String test();
}
Implémenter l'interface dans le contrôleur
@RestController
@Slf4j
public class TestApiController implements TestApi {
@Override
public String test() {
log.info("In Test");
return "Value";
}
}
Il peut être utilisé comme: Rest client
j'ai résolu ce problème.
SUR LE CLIENT:
J'utilise cette bibliothèque https://github.com/ggeorgovassilis/spring-rest-invoker/ . Cette bibliothèque génère un proxy à partir de l'interface pour appeler le service Spring Spring.
J'ai étendu cette bibliothèque:
J'ai créé une annotation et une classe de client d'usine:
Identifier un service de repos de printemps
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SpringRestService {
String baseUri();
}
Cette classe génère un repos client à partir d'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);
}
}
Je configure mon usine:
<bean class="it.eng.rete2i.springjsonmapper.factory.RestFactory">
<property name="basePackage" value="it.giancarlo.rest.services" />
</bean>
ON REST SIGNATURE DE SERVICE
c'est un exemple d'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 MISE EN ŒUVRE DU SERVICE
Mise en service
@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;
}
}
Pour résoudre les annotations sur les paramètres, je crée un RequestMappingHandlerMapping personnalisé qui ressemble à toutes les interfaces annotées avec @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;
}
}
}
}
J'ai créé une classe de configuration
@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;
}
}
et je l'ai configuré
<bean class="....WebConfig" />