I'm using Spring 4 with Spring Data MongoDB and want to get rid of some boilerplate code in my controllers.
I just want to replace this:
@RequestMapping("{id}")
void a(@PathVariable ObjectId id) {
DomainObject do = service.getDomainObjectById(id);
// ...
}
with this:
@RequestMapping("{id}")
void a(@PathVariable("id") DomainObject do) {
// ...
}
At the moment I've got to write a pair of PropertyEditorSupport
and @ControllerAdvice
classes for each domain object I have:
@Component
public class SomeDomainObjectEditor extends PropertyEditorSupport {
@Autowired
SomeDomainObjectService someDomainObjectService;
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(someDomainObjectService.getById(new ObjectId(text)));
}
@Override
public String getAsText() {
SomeDomainObject value = (SomeDomainObject) getValue();
return (value != null ? value.getId().toString() : null);
}
}
@ControllerAdvice
public class SomeDomainObjectControllerAdvice {
@Autowired
SomeDomainObjectEditor someDomainObjectEditor;
@InitBinder
public void register(WebDataBinder binder, WebRequest request) {
binder.registerCustomEditor(SomeDomainObject.class, someDomainObjectEditor);
}
}
And I can't figure out an easy way to get this done in a generic way, because I have a lot of domain objects and all behave the same.
All my domain objects implement BaseDocument<ID>
and thus have the getId()
method. So basically I want something like this:
public class BaseDocumentPropertyEditor extends PropertyEditorSupport { ... }
It would also be okay (= nice) to have this working, using a Converter<String, BaseDocument<?>>
which can be used also in other places within the Spring Framework.
My main problem is, that I can't imagine an easy way to find the corresponding @Service
in order to fetch the domain object from DB. (I can't use the Repository
because of access restriction for certain data).
Hopefully you have some advice. Thank you!
If you also want proper Exception handling, you should use DomainClassPropertyEditorRegistrar
, because DomainClassConverter
swallows underlying exceptions...
Here we go! Just update your WebMvcConfigurationSupport
with:
@Override
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter();
ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer) adapter.getWebBindingInitializer();
initializer.setPropertyEditorRegistrar(domainClassPropertyEditorRegistrar());
return adapter;
}
@Bean
public DomainClassPropertyEditorRegistrar domainClassPropertyEditorRegistrar() {
return new DomainClassPropertyEditorRegistrar();
}
(Maybe @Bean
is unnecessary, but at least it works this way)
Spring Data already provides everything I need: DomainClassConverter
Just put
@Bean
public DomainClassConverter<?> domainClassConverter() {
return new DomainClassConverter<FormattingConversionService>(mvcConversionService());
}
in the WebMvcConfigurationSupport
class and it all works out of the box!
My final solution was to just stay with the one pair of classes per domain object approach. I just built 2 abstract classes plus an interface, to minimize the effort:
1. PropertyEditor
public abstract class AbstractEntityEditor<ID extends Serializable, SERVICE extends CanGetEntityById<?, ID>> extends PropertyEditorSupport {
@Autowired
SERVICE service;
@Autowired
ConversionService cs;
final Class<ID> id;
public AbstractEntityEditor(Class<ID> id) {
this.id = id;
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(service.getById(cs.convert(text, id)));
}
}
2. ControllerAdvice
public abstract class AbstractEntityEditorControllerAdvice<EDITOR extends PropertyEditor> {
@Autowired
EDITOR editor;
final Class<?> entity;
public AbstractEntityEditorControllerAdvice(Class<?> entity) {
this.entity = entity;
}
@InitBinder
public void register(WebDataBinder binder, WebRequest request) {
binder.registerCustomEditor(entity, editor);
}
}
3. Service interface to retrieve an domain object
public interface CanGetEntityById<ENTITY, ID extends Serializable> {
ENTITY getById(ID id) throws NotFoundException;
}
And here's a sample use case:
1.
@Component
public class UserEditor extends AbstractEntityEditor<ObjectId, UserService> {
public UserEditor() {
super(ObjectId.class);
}
}
2.
@ControllerAdvice
public class UserControllerAdvice extends AbstractEntityEditorControllerAdvice<UserEditor>{
public UserControllerAdvice() {
super(User.class);
}
}
3.
public interface UserService extends GetEntityById<User, ObjectId> { }
4.
@Service
public class UserServiceImpl implements UserService {
public User getById(ObjectId id) throws NotFoundException {
// fetch User from repository and return
}
}
Maybe there's a way to make it a bit better, but at least it works! And now it's just 5 lines of code to write :-)
.