Currently RequestMappingHandlerMapping
shares a global EmbeddedValueResolver
.
https://github.com/spring-projects/spring-framework/blob/4a9c7e631c60a12f3426d42143e9e27be0bf68ab/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java#L182-L185
It's not possible to extract a base controller path which contains controller specific variables.
import java.io.Serializable;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.ResolvableType;
import org.springframework.data.jpa.domain.AbstractPersistable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.DeleteMapping;
public abstract class AbstractEntityController<T extends AbstractPersistable<PK>, PK extends Serializable>
implements ApplicationContextAware {
private final Class<T> entityClass;
private JpaRepository<T, PK> repository;
@SuppressWarnings("unchecked")
protected AbstractEntityController() {
entityClass = (Class<T>) ResolvableType.forClass(getClass()).as(AbstractEntityController.class).getGeneric(0)
.resolve();
}
@SuppressWarnings("unchecked")
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
repository = (JpaRepository<T, PK>) applicationContext.getBeanProvider(
ResolvableType.forClassWithGenerics(JpaRepository.class, entityClass, ResolvableType
.forClass(getClass()).as(AbstractTreeableEntityController.class).getGeneric(1).resolve()),
false).getIfUnique();
}
public String getEntityName() {
return StringUtils.uncapitalize(entityClass.getSimpleName());
}
@DeleteMapping("/#{#this.entityName}/{id}")
public void delete(PK id) {
repository.deleteById(id);
}
}
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'enityName' cannot be found on object of type 'org.springframework.beans.factory.config.BeanExpressionContext' - maybe not public or not valid?
at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:217)
at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:104)
at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:91)
at org.springframework.expression.spel.ast.SpelNodeImpl.getTypedValue(SpelNodeImpl.java:117)
at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:308)
at org.springframework.expression.common.CompositeStringExpression.getValue(CompositeStringExpression.java:108)
at org.springframework.expression.common.CompositeStringExpression.getValue(CompositeStringExpression.java:43)
at org.springframework.context.expression.StandardBeanExpressionResolver.evaluate(StandardBeanExpressionResolver.java:166)
... 57 more
Comment From: quaff
@rstoyanchev It's a very useful feature, please provide built-in supports or expose extension to application code, thanks.
Comment From: quaff
Prototype created: https://github.com/spring-projects/spring-framework/pull/27324
Comment From: bclozel
Functional handlers are much more in line with that programmatic approach. I'm not sure we want to add that level of complexity (this will also be challenging for tools like IDEs).
Comment From: quaff
Currently placeholders and SPEL is supported, IDE handle this already. If the team decide to decline, please guide me how to implement it at application level, I real don't want override methods just adding annotations like this:
@TestComponent
@RestController
@Validated
public class TestEntityController extends AbstractEntityController<TestEntity, Long> {
public static final String PATH_LIST = "/testEntities";
public static final String PATH_DETAIL = "/testEntity/{id:\\d+}";
@Autowired
private TestEntityRepository testEntityRepository;
@Override
@JsonView({ View.List.class })
@GetMapping(PATH_LIST)
public ResultPage<TestEntity> list(@PageableDefault(sort = "id", direction = DESC) Pageable pageable,
@RequestParam(required = false) String query, @ApiIgnore TestEntity example) {
return super.list(pageable, query, example);
}
@Override
@PostMapping(PATH_LIST)
public TestEntity save(@RequestBody @JsonView(View.Creation.class) @Valid TestEntity testEntity) {
return super.save(testEntity);
}
@Override
@GetMapping(PATH_DETAIL)
public TestEntity get(@PathVariable Long id) {
return super.get(id);
}
@Override
@PutMapping(PATH_DETAIL)
public void update(@PathVariable Long id, @RequestBody @JsonView(View.Update.class) @Valid TestEntity testEntity) {
super.update(id, testEntity);
}
@Override
@PatchMapping(PATH_DETAIL)
public TestEntity patch(@PathVariable Long id,
@RequestBody @JsonView(View.Update.class) @Valid TestEntity testEntity) {
return super.patch(id, testEntity);
}
@Override
@DeleteMapping(PATH_DETAIL)
public void delete(@PathVariable Long id) {
super.delete(id);
}
}
It should be:
@TestComponent
@RestController
@Validated
public class TestEntityController extends AbstractEntityController<TestEntity, Long> {
}
Comment From: quaff
Graceful solution using ThreadLocal
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.StringValueResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
public class SmartRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private static ThreadLocal<Object> handlerHolder = new ThreadLocal<>();
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
super.setEmbeddedValueResolver(new StringValueResolver() {
@Override
public String resolveStringValue(String strVal) {
Object handler = handlerHolder.get();
if (handler != null) {
strVal = String.valueOf(PARSER.parseExpression(strVal, ParserContext.TEMPLATE_EXPRESSION)
.getValue(new StandardEvaluationContext(handler)));
}
if (resolver != null) {
strVal = resolver.resolveStringValue(strVal);
}
return strVal;
}
});
}
@Override
protected void detectHandlerMethods(Object handler) {
handlerHolder.set((handler instanceof String ? obtainApplicationContext().getBean((String) handler) : handler));
super.detectHandlerMethods(handler);
handlerHolder.remove();
}
}
Comment From: rstoyanchev
What about extracting the prefix to a class-level mapping:
@RestController
@Validated
@RequestMapping("/testEntities")
public class TestEntityController extends AbstractEntityController<TestEntity, Long> {
}
and leaving the base class to complete the mapping at the method level:
public abstract class AbstractEntityController<T extends AbstractPersistable<PK>, PK extends Serializable>
implements ApplicationContextAware {
@DeleteMapping("/{id}")
public void delete(PK id) {
repository.deleteById(id);
}
}
Comment From: quaff
What about extracting the prefix to a class-level mapping:
```java @RestController @Validated @RequestMapping("/testEntities") public class TestEntityController extends AbstractEntityController
{ } ```
and leaving the base class to complete the mapping at the method level:
```java public abstract class AbstractEntityController
, PK extends Serializable> implements ApplicationContextAware { @DeleteMapping("/{id}") public void delete(PK id) { repository.deleteById(id); }
} ``` Some paths are singulars and others are plurals
protected static final String PATH_LIST = "/#{T(com.example.StringHelper).pluralOf(entityName)}";
protected static final String PATH_DETAIL = "/#{entityName}/{id}";
Comment From: bclozel
This still look a lot like spring-data-rest, but still missing a lot of capabilities and features. I don't think we should apply this change because the use case would be served much better by spring-data-rest or the infrastructure that spring-data-rest is using.
Comment From: quaff
I'd prefer writing my own controllers rather than using spring-data-rest
, I fixed it by extending RequestMappingHandlerMapping
.
Comment From: rstoyanchev
You can write your own controllers with Spring Data REST but only when needed.