Spring Data Rest Override Repositories (Controllers vs AOP) - java

Domain/Repository
Project {
User owner;
}
//Querydsl repositories
#RepositoryRestResource
public interface ProjectRepository extends PagingAndSortingRepository<Project, Long>, QueryDslPredicateExecutor<Project>, QuerydslBinderCustomizer<QProject> {
default void customize(QuerydslBindings bindings, QProject project) {
(...)
}
}
Requeriment:
filter data according to the authenticated user context:
If user is ROLE_PUBLIC show projects according predicate and where user is the owner.
If user is ROLE_ADMIN show projects according predicate filter.
I tried solved throught several alternatives:
Option 1: Override #RepositoryRestController as says Spring DATA REST doc:
#RepositoryRestController
public class ProjectController {
#RequestMapping(value = "/projects", method = RequestMethod.GET)
#ResponseBody
public PagedResources<?> search(
#QuerydslPredicate(root=Project.class ,bindings =ProjectRepository.class) Predicate predicate,
#PageableDefault Pageable pageable, //
#AuthenticationPrincipal Principal principal) {
(...) // Update predicate wit user context
projectRepository.findAll(predicate,pageagle);
(...)
}
}
This throw exception since the #QueryDslPredicat is not on the RepositoryRestHandlerAdapter list (DATAREST-838):
Failed to instantiate [com.querydsl.core.types.Predicate]: Specified class is an interface
The most similar argumentResolver into the list is QuerydslAwareRootResourceInformationHandlerMethodArgumentResolver but I dont know if it could be useful for this issue.
Option 2: This issue is a tipical cross-cutting concern so I tried apply AOP. Define Advice and update the args (Predicate):
#Around("this(com.xxx.yyy.repository.ProjectRepository)")
public void filterProjectsByUser(final ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
// .. Find args Predicate, update it adding user profile expression and procceed.
pjp.proceed(args);
}
The result is not the same as default Repository methods, instead of original JSON Object (with _embedded,page,_links), the response was:
{
"_links" : {
"profile" : {
"href" : "http://localhost:8087/api/profile/projects"
}
}
}
Option 3
Using #RestController:
#RestController
public class ProjectController {
#RequestMapping(value = "/projects/search", method = RequestMethod.GET)
#ResponseBody
public PagedResources<?> search(
#QuerydslPredicate(root=Project.class ,bindings =ProjectRepository.class) Predicate predicate,
#PageableDefault Pageable pageable, //
#AuthenticationPrincipal Principal principal) {
(...) // Update predicate wit user context
projectRepository.findAll(predicate,pageagle);
(...)
}
}
Using the same path #RequestMapping("/projects",...) also override other endpoints (PUT,POST,...) and this is not desired. This forced to define another endpoint ("projects/search").
I think that this workaround is not elegant and requires news endpoints. I am sure that exist a better alternative with Spring Data REST.
Questions:
Any sugestions about how to solve this requeriment?
How apply AOP to Spring Data REST to solve crosscutting requirements?
Why #QuerydslPredicate at argument resolver?
Spring Data REST version 2.5.6
Although it has been resolved I would like to receive feedback and suggestions . I hope that QueryDSLPredicate annotation will be fixedn and the documentation will be improved with more samples about this issue.

Finally I get run the Option 2 my error was not return proceed invocation result:
#Aspect
#Transactional
#Component
public class FilterProjectsAspect {
#Pointcut("execution(* com.xxx.ProjectRepository.findAll(..))")
public void projectFindAll() {
}
#Around("projectFindAll()")
public Object filterProjectsByUser(final ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Predicate) {
Predicate predicate=(Predicate) args[i];
BooleanExpression isProjectOwner =buildExpressionForUser()
predicate = ExpressionUtils.allOf(isProjectOwner, predicate);
args[i]=predicate; //Update args
}
return pjp.proceed(args);
}
}

Related

How do I create an annotation in Java and access variables it creates?

I'm using Spring Boot to create a pretty standard REST service. Part of that means that a lot of methods will return pages of results, so the input into the method will require page number, page size, etc - e.g.:
#GetMapping(
value = "",
produces = MediaType.APPLICATION_JSON_VALUE
)
#ResponseBody
ResponseEntity readAll(
#RequestParam("pn") Integer pageNumber,
#RequestParam("ps") Integer pageSize,
#RequestParam("sc") String sortColumn,
#RequestParam("so") String sortOrder
) {
Then in the method itself we'll validate the parameters (e.g. make sure page size < 100, sort column is an allowed value, etc). Then convert the values into something useable - e.g. create a Sort object from column and order, and then a Pageable object from the page number, page size and sort object. That finally gets passed into the JPA repository to return a page of results.
Since this pattern is going to get repeated over and over, I'd like to just make an annotation that encapsulates all of this, but I'm not sure how to convert the 4 RequestParam variables into a single Pageable object, or how to access that object created in the annotation in the method body.
I've tried some basic Annotation work, e.g.
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = ServicePageSizeValidator.class)
public #interface ServicePageSize {
Integer DEFAULT_MAX = 100;
String max() default "100";
}
class ServicePageSizeValidator
implements ConstraintValidator<ServicePageSize, Integer> {
private static final Logger LOGGER =
LoggerFactory.getLogger(ServicePageSizeValidator.class);
private Integer max = ServicePageSize.DEFAULT_MAX;
#Override
public void initialize(final ServicePageSize annotation) {
try {
max = Integer.valueOf(annotation.max());
} catch (Exception e) {
LOGGER.warn(
"Exception caught trying to parse page size " +
"constraint {}.", annotation.max(), e
);
max = ServicePageSize.DEFAULT_MAX;
}
}
#Override
public boolean isValid(final Integer pageSize,
final ConstraintValidatorContext context) {
try {
if (pageSize == null) {
throw new InvalidPageSizeException("Null page size.");
}
if (pageSize > max) {
throw new InvalidPageSizeException(
"Page size " + pageSize + "larger than maximum " +
"allowed (" + max + ")."
);
}
} catch (Exception e) {
LOGGER.warn("Invalid page size.", e);
return false;
}
return true;
}
}
And that generally seems to work, but it's operating only on a single parameter, and just failing the whole annotation if the parameter is wrong - there's not way for me to access information about the validation in the method assuming it passed.
As mentioned by #DrTeeth, Spring already provides a great Pagination functionality, so I would stick with it, because it gives you options that you can also use with Repositories. Pageable is definitely very useful.
Generally speaking, you can perform this kind of validation using the JEE Validation API. Also, you can use Spring Resolvers to provide the params in every controller that needs them, in a very easy way. Let me give you an example. First of all, let's create a class that maps your params:
public class Pagination {
#Max(100)
int pageNumber;
int pageSize;
String sortColumn;
String sortOrder;
}
As you can see, I'm already using Validation API. You can add the capability to your Spring Project by adding this dependency in your pom:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
For the example's purposes, I've only added the validation on page number.
You can now create an interface. I'll explain it's use later.
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.PARAMETER)
public #interface PaginationConfig {
}
This will tell Spring that it can expect the usage of PaginationConfig as a method's parameter's decorator.
It's now time for our resolver:
public class PaginationResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterAnnotation(PaginationConfig.class) != null;
}
#Override
public Object resolveArgument(
MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) throws BadRequestException {
Pagination pagination = new Pagination();
HttpServletRequest request
= (HttpServletRequest) nativeWebRequest.getNativeRequest();
try {
pagination.setPageNumber(Integer.parseInt(request.getParameter("pn")));
pagination.setPageSize(Integer.parseInt(request.getParameter("ps")));
} catch (NumberFormatException e) {
throw new BadRequestException();
}
pagination.setSortColumn(request.getParameter("sc"));
pagination.setSortOrder(request.getParameter("so"));
return pagination;
}
}
It is bounded to the interface we just created, and it reads the parameters values from the request, creating a Pagination object. Note that the validation is not happening, yet. We are just telling Spring how to read those values, and catching the NumberFormatException, so we can translate it in a custom Exception (that I've called BadRequestException in this example and that I would map on BAD_REQUEST HTTP Response code).
We're almost done. We now have to add some configurations:
#Configuration
public class MyConfig implements WebMvcConfigurer {
#Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new PaginationResolver());
}
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
In this way, we are telling Spring that it has to use out custom resolver and that is has to validate methods' parameters.
It's now time we go into our controller. I'll give you an example:
#RestController
#Validated
public class MyController {
#GetMapping
public String validateAndPaginate(#Valid #PaginationConfig Pagination pagination) {
return "hello world " + pagination.getPageSize();
}
}
Things to be noted:
we need to use #Validated at class level;
we need to use #Valid at parameter level;
we are decorating the parameter with #PaginationConfig, the interface we created. Spring knows how to find the values that needs to bind in that object, because we've told it to use our resolver;
we are using the Validation API, so everything will be validated based on the annotations we've put into Pagination class;
if a validation fails, Spring will give us a 500 error, but you can easily map it into everything you want.
This might still be reasonable to keep open as a question, as there are probably similar situations for other annotations that are needed. But for pagination at least Spring Boot apparently has me covered.
(external link): https://reflectoring.io/spring-boot-paging/
Essentially:
ResponseEntity readAll(Pageable pageable)
will work, and there are a variety of ways the URL parameter names can be configured, e.g. in application.properties:
spring.data.web.pageable.size-parameter=size
spring.data.web.pageable.page-parameter=page
spring.data.web.pageable.default-page-size=20
spring.data.web.pageable.max-page-size=2000
or via annotation:
ResponseEntity readAll(
#PageableDefault(page = 0, size = 20) Pageable pageable
)
I'm going to leave this as open, and this as not the accepted answer as I feel like the more general question of how to do this right is still valid. But it looks like the answer is probably to create a class that will self-map request parameters to itself, and then use annotations on that single object passed in to configure it.

How to map custom/dynamic requests to a controller (Spring)?

How can I map custom/dunamic requests toa given controller, based on a repository lookup?
The use-case is a CMS-like feature in a web-platform, where certain URL patterns ("pages") stored in the DB should be handled by a separate controller PageController.java. These patterns are not necessarily known at compile-time, and they can also be added and modified while the app is deployed (thus, it cannot be annotation-driven).
I did try to map a controller to "**" (see below), but that did not work for 2 reasons: firstly all other requests resolved to that same controller method (I had hoped that it would use "**" as a fallback and try the others first), and it also ended up resolving all requests to my static/asset files to this controller (resulting in unwanted 404-responses).
#Controller
public class PageController {
#Inject
private PageService pageService;
#RequestMapping(value = "**", method = RequestMethod.GET)
public String getPage(Model model, HttpServletRequest request, #CurrentUser User user) {
String path = request.getRequestURI();
Page page = this.pageService.getByPath(path, user);
if (page == null) {
throw new NotFoundException();
}
model.addAttribute("page", page);
return "web/page";
}
}
The temporary work-around/modification to the above method has so far been to map a pre-defined URL-prefixes to this controller (eg. /page/**, /info/**, /news/** etc), but this is an inelegant solution that adds arbitrary limitations to the system which I now seek to eliminate.
I am currently using Spring Boot 2.0. In addition to the naive mapping to ** in a regular #Controller class (using the #RequestMapping -annotation), I have also tried configuring SimpleUrlHandlerMapping the following way:
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Inject
private PageDao pageDao;
#Bean
public PageController pageController() {
return new PageController();
}
#Bean
public SimpleUrlHandlerMapping pageUrlHandlerMapping() {
SimpleUrlHandlerMapping pageUrlHandlerMapping = new SimpleUrlHandlerMapping();
PageController pageController = this.pageController();
Map<String, Object> urlMap = this.pageDao.findAll().stream()
.map(Page::getNormalizedSlug)
.collect(Collectors.toMap(Function.identity(),
slug -> pageController, (existing, duplicate) -> existing));
pageUrlHandlerMapping.setUrlMap(urlMap);
pageUrlHandlerMapping.setOrder(Ordered.HIGHEST_PRECEDENCE); // <- Cannot be LOWEST_PRECEDENCE for some reason...
return pageUrlHandlerMapping;
}
}
public class PageController implements Controller {
#Inject
private PageService pageService;
#Inject
private DmaWebControllerAdvice controllerAdvice;
#Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
User user = null;
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof User) {
user = (User) principal;
}
String path = request.getRequestURI();
Page page = this.pageService.getByPath(path, user);
if (page == null) {
throw new NotFoundException();
}
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("web/page");
modelAndView.addObject("page", page);
controllerAdvice.globalModelAttributes(modelAndView.getModel(), null);
return modelAndView;
}
}
This approach does technically work, but the list of pages will somehow have to be reloaded into the SimpleUrlHandlerMapping whenever one of the pages is changed (i am not quite sure how to do that). This also possibly overwrites some default Spring Boot-configuration, that I would ideally like to keep. It also has some drawbacks compared to resolving controllers using #Controller and #RequesMapping because I currently am injecting certain data into all views resolved that way (mainly model-data used in the overall design of the website, like menu, quicklinks etc). In the above attempt, I have had to set those via a separate call to controllerAdvice-globalModelAttributes().
What I am seeking is a solution where my repository is queried for potential page-matches in runtime, and if it is valid then the request will be handled by the proper page-controller. Is a custom HandlerMapping -implementation the way to do this? And if not, how should I solve this? And if making a separate HandlerMapping for pages, how do I add/register this in my configuration without overwriting the default provided by Spring?
Why don't you just implement a catch-all controller which parses your patterns as a parameter, does a db look-up and then use a forward to specific controllers (info, page, news etc.)? Seems like for a CMS, this look-up logic belongs into your code (e.g. service layer).
Easiest(but not the best) way to achieve what you need is creating custom HandlerMapping implementation:
public class PageMapper implements HandlerMapping, Ordered {
private HandlerMethod handlerMethod;
public CustomMapper(Object controller, Method method) {
this.handlerMethod = new HandlerMethod(controller, method);
}
#Override
public HandlerExecutionChain getHandler(HttpServletRequest httpServletRequest) throws Exception {
return new HandlerExecutionChain(handlerMethod);
}
#Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE; //you have to add the handler to the end
}
}
Now remove #Controller annotation from PageController because you don't need it to be detected automatically anymore. After that register controller and mapping to config:
#Configuration
public class AppWebConfig implements WebMvcConfigurer {
#Bean
public PageController pageController() {
return new PageController();
}
#Bean
public HandlerMapping pageMapping(PageController pageController) {
Method method = BeanUtils.resolveSignature("getPage", PageController.class);
return new PageMapping(pageController, method);
}
}
Now every request unrecognized by other HandlerMapping instances will be sent to your mapping hence to your controller. But this approach has obvious disadvantage. Since your mapping is the last in the chain of mappings you never get 404 error. Therefor you never know about something wrong with you resources (e.g. if some of them are missing).
I would prefer let application to distinguish paths by prefix (just like you do it already), where prefix is operation application is going to do with a page. For example if you need to show or edit the page:
#Controller
public class PageController {
private final static String SHOW = "/show";
private final static String EDIT = "/edit";
#Inject
private PageService pageService;
GetMapping(value = SHOW + "/**")
public String getPage(Model model, HttpServletRequest request, #CurrentUser User user) {
String path = request.getRequestURI().substring(SHOW.length());
Page page = this.pageService.getByPath(path, user);
...
model.addAttribute("page", page);
return "web/page";
}
//the same for EDIT operation
}

Spring Data Rest / Spring Hateoas Custom Controller - PersistentEntityResourceAssembler

I'm attempting to add some additional business logic to the auto-generated endpoints from the RepositoryRestResource. Please see the code below:
Resource:
#RepositoryRestResource(collectionResourceRel="event", path="event")
public interface EventRepository extends PagingAndSortingRepository<Event, Long> {
}
Controller:
#RepositoryRestController
#RequestMapping(value = "/event")
public class EventController {
#Autowired
private EventRepository eventRepository;
#Autowired
private PagedResourcesAssembler<Event> pagedResourcesAssembler;
#RequestMapping(method = RequestMethod.GET, value = "")
#ResponseBody
public PagedResources<PersistentEntityResource> getEvents(Pageable pageable,
PersistentEntityResourceAssembler persistentEntityResourceAssembler) {
Page<Event> events = eventRepository.findAll(pageable);
return pagedResourcesAssembler.toResource(events, persistentEntityResourceAssembler);
}
}
I've looked at the following two stackoverflow articles:
Can I make a custom controller mirror the formatting of Spring-Data-Rest / Spring-Hateoas generated classes?
Enable HAL serialization in Spring Boot for custom controller method
I feel like I am close, but the problem that I am facing is that:
return pagedResourcesAssembler.toResource(events, persistentEntityResourceAssembler);
returns an error saying:
"The method toResource(Page<Event>, Link) in the type PagedResourcesAssembler<Event> is not applicable
for the arguments (Page<Event>, PersistentEntityResourceAssembler)".
The toResource method has a method signature that accepts a ResourceAssembler, but I'm not sure how to properly implement this and I can't find any documentation on the matter.
Thanks in advance,
- Brian
Edit
My issue was that I thought I could override the controller methods that are auto-created from #RepositoryRestResource annotation without having to create my own resource and resource assembler. After creating the resource and resource assembler I was able to add my business logic to the endpoint.
Resource:
public class EventResource extends ResourceSupport {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Resource Assembler:
#Component
public class EventResourceAssembler extends ResourceAssemblerSupport<Event, EventResource> {
public EventResourceAssembler() {
super(EventController.class, EventResource.class);
}
#Override
public EventResource toResource(Event entity) {
EventResource eventResource = createResourceWithId(entity.getId(), entity);
eventResource.setName(entity.getName());
return eventResource;
}
}
Updated Controller:
#RepositoryRestController
#RequestMapping(value = "/event")
public class EventController {
#Autowired
private EventRepository eventRepository;
#Autowired
private EventResourceAssembler eventResourceAssembler;
#Autowired
private PagedResourcesAssembler<Event> pageAssembler;
#RequestMapping(method = RequestMethod.GET, value = "")
#ResponseBody
public PagedResources<EventResource> getEvents(Pageable pageable) {
Page<Event> events = eventRepository.findAll(pageable);
// business logic
return pageAssembler.toResource(events, eventResourceAssembler);
}
}
The thing I don't like about this is that it seems to defeat the purpose of having a RepositoryRestResource. The other approach would be to use event handlers that would get called before and/or after the create, save, delete operations.
#RepositoryEventHandler(Event.class)
public class EventRepositoryEventHandler {
#HandleBeforeCreate
private void handleEventCreate(Event event) {
System.out.println("1");
}
}
There doesn't seem to be any events for the findAll or findOne operations. Anyways, both these approaches seem to solve my problem of extending the auto generated controller methods from RepositoryRestResource.
It requires a PagedResourcesAssembler, Spring will inject one for you if you ask.
public PagedResources<Foo> get(Pageable page, PagedResourcesAssembler<Foo> assembler) {
// ...
}
In this case the resource is Foo. It seems in your case the resource you're trying to return is an Event. If that's so, I would expect your code to look something like:
private ResourceAssembler<Event> eventAssembler = ...;
public PagedResources<Event> get(Pageable page, PagedResourcesAssembler<Event> pageAssembler) {
Event event = ...;
return eventAssembler.toResource(event, pageAssembler);
}
You provide the ResourceAssembler<Event> that tells Spring how to turn Event into a Resource. Spring injects the PagedResourcesAssembler<Event> into your controller method to handle the pagination links. Combine them by calling toResource and passing in the injected pageAssembler.
The final result can be returned simply as a body as above. You could also use things like HttpEntity to gain more control over status codes and headers.
Note: The ResourceAssembler you provide can literally be something as simple as wrapping the resource, such as Event, with a Resource object. Generally you'll want to add any relevant links though.
To hack it you can use just PagedResourcesAssembler<Object> like:
#RequestMapping(method = RequestMethod.GET, value = "")
#ResponseBody
public PagedModel<PersistentEntityResource> getEvents(
Pageable pageable,
PersistentEntityResourceAssembler persistentAssembler,
PagedResourcesAssembler<Object> pageableAssembler
) {
return pageableAssembler.toModel(
(Page<Object>) repository.findAll(pageable),
persistentAssembler
);
}

Multiple Spring MVC validator for the same Controller

I am having a Spring controller with a Validator defined as:
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.setValidator(new MyValidator(myService));
}
And calling it:
public ResponseEntity<?> executeSomething(
#ApiParam(name = "monitorRequest", required = true, value = "") #Valid #RequestBody MonitorRequest monitorRequest,
HttpServletRequest request, HttpServletResponse response) throws RESTException
I need to add one more Validator for this controller that could be called from some specific methods of this controller. Is there any way to achieve this?
EDIT: I am handling the Error by:
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseBody
public ResponseEntity<?> processValidationError(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrors = result.getFieldErrors();
ValidationErrorObj obj = processFieldErrors(fieldErrors);
ResponseEntity r = new ResponseEntity(obj, HttpStatus.BAD_REQUEST);
return r;
}
You can have more than one InitBinder method in a controller. It is controlled by the optional value parameter . For the javadoc of InitBinder : String[] value : The names of command/form attributes and/or request parameters that this init-binder method is supposed to apply to ... Specifying model attribute names or request parameter names here restricts the init-binder method to those specific attributes/parameters, with different init-binder methods typically applying to different groups of attributes or parameters.
Another way would be to explicely call a complementary Validator in specific methods.
BTW : I can't see any Errors or BindingResult in your controller method signature : where do you find whether errors occured ?
For those who are still trying to figure out how to solve this in 2017. I was facing similar issues while trying to implement 2 validators in my RestController. I followed the approach mentioned above by #Serge Ballasta.
I ended up making 2 Model each of linked to their specific Validators. The Controller methods look something like
#RequestMapping(value = "register", method = RequestMethod.POST)
public ResponseEntity<User> register(#Valid #RequestBody UserRegisterRequest userRegisterRequest) {
return null;
}
#RequestMapping(value = "test", method = RequestMethod.POST)
public ResponseEntity<?> test(#Valid #RequestBody TestRequest testRequest) {
return null;
}
and I created 2 initBinders to wire these validators in the controller like
#InitBinder("testRequest")
public void setupBinder(WebDataBinder binder) {
binder.addValidators(testValidator);
}
#InitBinder("userRegisterRequest")
public void setupBinder1(WebDataBinder binder) {
binder.addValidators(userRegistrationRequestValidator);
}
Please note that the #RequestBody attributes (userRegisterRequest , testRequest) had to be provided as values in the #InitBinder() annotations.
By the way the in my code I handle the bindingResult in a custom ExceptionHandler class which extends ResponseEntityExceptionHandler which gives me freedom to do custom handling of the response.

Spring MVC: bind request attribute to controller method parameter

In Spring MVC, it is easy to bind request parameter to method paramaters handling the request. I just use #RequestParameter("name"). But can I do the same with request attribute? Currently, when I want to access request attribute, I have to do following:
MyClass obj = (MyClass) request.getAttribute("attr_name");
But I really would like to use something like this instead:
#RequestAttribute("attr_name") MyClass obj
Unfortunately, it doesn't work this way. Can I somehow extend Spring functionality and add my own "binders"?
EDIT (what I'm trying to achieve): I store currently logged user inside request attribute. So whenever I want to access currently logged user (which is pretty much inside every method), I have to write this extra line user = (User) request.getAttribute("user");. I would like to make it as short as possible, preferably inject it as a method parameter. Or if you know another way how to pass something across interceptors and controllers, I would be happy to hear it.
Well, I finally understood a little bit how models work and what is #ModelAttribute for. Here is my solution.
#Controller
class MyController
{
#ModelAttribute("user")
public User getUser(HttpServletRequest request)
{
return (User) request.getAttribute("user");
}
#RequestMapping(value = "someurl", method = RequestMethod.GET)
public String HandleSomeUrl(#ModelAttribute("user") User user)
{
// ... do some stuff
}
}
The getUser() method marked with #ModelAttribute annotation will automatically populate all User user parameters marked with #ModelAttribute. So when the HandleSomeUrl method is called, the call looks something like MyController.HandleSomeUrl(MyController.getUser(request)). At least this is how I imagine it. Cool thing is that user is also accessible from the JSP view without any further effort.
This solves exactly my problem however I do have further questions. Is there a common place where I can put those #ModelAttribute methods so they were common for all my controllers? Can I somehow add model attribute from the inside of the preHandle() method of an Interceptor?
Use (as of Spring 4.3) #RequestAttribute:
#RequestMapping(value = "someurl", method = RequestMethod.GET)
public String handleSomeUrl(#RequestAttribute User user) {
// ... do some stuff
}
or if the request attribute name does not match the method parameter name:
#RequestMapping(value = "someurl", method = RequestMethod.GET)
public String handleSomeUrl(#RequestAttribute(name="userAttributeName") User user) {
// ... do some stuff
}
I think what you are looking for is:
#ModelAttribute("attr_name") MyClass obj
You can use that in the parameters for a method in your controller.
Here is a link a to question with details on it What is #ModelAttribute in Spring MVC?
That question links to the Spring Documentation with some examples of using it too. You can see that here
Update
I'm not sure how you are setting up your pages, but you can add the user as a Model Attribute a couple different ways. I setup a simple example below here.
#RequestMapping(value = "/account", method = RequestMethod.GET)
public ModelAndView displayAccountPage() {
User user = new User(); //most likely you've done some kind of login step this is just for simplicity
return new ModelAndView("account", "user", user); //return view, model attribute name, model attribute
}
Then when the user submits a request, Spring will bind the user attribute to the User object in the method parameters.
#RequestMapping(value = "/account/delivery", method = RequestMethod.POST)
public ModelAndView updateDeliverySchedule(#ModelAttribute("user") User user) {
user = accountService.updateDeliverySchedule(user); //do something with the user
return new ModelAndView("account", "user", user);
}
Not the most elegant, but works at least...
#Controller
public class YourController {
#RequestMapping("/xyz")
public ModelAndView handle(
#Value("#{request.getAttribute('key')}") SomeClass obj) {
...
return new ModelAndView(...);
}
}
Source : http://blog.crisp.se/tag/requestattribute
From spring 3.2 it can be done even nicer by using Springs ControllerAdvice annotation.
This then would allow you to have an advice which adds the #ModelAttributes in a separate class, which is then applied to all your controllers.
For completeness, it is also possible to actually make the #RequestAttribute("attr-name") as is.
(below modified from this article to suit our demands)
First, we have to define the annotation:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.PARAMETER)
public #interface RequestAttribute {
String value();
}
Then we need a [WebArgumentResolver] to handle what needs to be done when the attribute is being bound
public class RequestAttributeWebArgumentResolver implements WebArgumentResolver {
public Object resolveArgument(MethodParameter methodParameter, NativeWebRequest nativeWebRequest) throws Exception {
// Get the annotation
RequestAttribute requestAttributeAnnotation = methodParameter.getParameterAnnotation(RequestAttribute.class);
if(requestAttributeAnnotation != null) {
HttpServletRequest request = (HttpServletRequest) nativeWebRequest.getNativeRequest();
return request.getAttribute(requestAttributeAnnotation.value);
}
return UNRESOLVED;
}
}
Now all we need is to add this customresolver to the config to resolve it:
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="customArgumentResolver">
<bean class="com.sergialmar.customresolver.web.support.CustomWebArgumentResolver"/>
</property>
</bean>
And we're done!
Yes, you can add your own 'binders' to the request attribute - see spring-mvc-3-showcase, or use #Peter Szanto's solution.
Alternatively, bind it as a ModelAttribute, as recommended in other answers.
As it's the logged-in user that you want to pass into your controller, you may want to consider Spring Security. Then you can just have the Principle injected into your method:
#RequestMapping("/xyz")
public String index(Principal principle) {
return "Hello, " + principle.getName() + "!";
}
In Spring WebMVC 4.x, it prefer implements HandlerMethodArgumentResolver
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(RequestAttribute.class) != null;
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return webRequest.getAttribute(parameter.getParameterAnnotation(RequestAttribute.class).value(), NativeWebRequest.SCOPE_REQUEST);
}
}
Then register it in RequestMappingHandlerAdapter

Categories