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

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
}

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.

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
);
}

Getting a list of all JSP pages in the catalog?

I have a service which must verify if a given page exists in WEB-INF/pages/info/ catalog. But I don't know how to implement this task. Are there any suggestions how to implement it?
UPD:
My rough implementation
#Service
public class ValidateUserAccessToPageService {
public boolean validate(String page, HttpServletRequest req) {
if (!req.getRequestURI().equals("/favicon.ico")) {
ServletContext ctx = req.getServletContext();
Set<String> pages = ctx.getResourcePaths("/WEB-INF/pages/info/");
for (String p : pages) {
if (getLogicName(p).equals(page)){
System.out.println("Found matching!");
}
}
}
return false;
}
String getLogicName(String page) {
return page.substring(page.lastIndexOf("/") + 1, page.lastIndexOf(".jsp"));
}
}
And controller logic
#Controller
public class RedirectController {
#Autowired
ValidateUserAccessToPageService service;
#RequestMapping ("/{page}")
String redirect (#PathVariable String page, HttpServletRequest req) {
return service.validate(page, req) ? page : "error";
}
}
You can use ServletContext#getResource() method to check resource (e.g. JSP file) existence. This method will return null for resources which does not exist.
But be careful - your implementation as is might introduce some security problems. If you allow user to specify any file, he might be able to get for example application configuration out of /WEB-INF/classes/myconfig.properties. If you really want to implement something like this, I would validate the page parameter to be just alphanumeric and hyphen for example (i.e. user would provide you only with /WEB-INF/pages/{pageParam}.jsp).

Spring programmatically register RequestMapping

I have a working url as this: localhost/info
#Controller
#RequestMapping("/info")
public class VersionController {
#RequestMapping(value = "", method = RequestMethod.GET)
public #ResponseBody
Map get() {
loadProperties();
Map<String, String> m = new HashMap<String, String>();
m.put("buildTimestamp", properties.getProperty("Application-Build-Timestamp"));
m.put("version", properties.getProperty("Application-Version"));
return m;
}
}
and I would to register some other mappings at initializing of my application as this:
localhost/xxxx/info
localhost/yyyy/info
localhost/zzzz/info
All these urls will return same response as localhost/info
The xxxx, yyyy part of the application is changeable. I have to register custom mappings as
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("???").setViewName("???");
}
Bu this is only working for views.
Any idea for dynamic registration?
You can register a new HandlerMapping where you can add the handlers for your URL paths; the most convenient implementation would be SimpleUrlHandlerMapping.
If you want those handlers to be bean methods (like those annotated with #RequestMapping) you should define them as HandlerMethod wrappers so that the already registered RequestMappingHandlerAdapter will invoke them.
As of Spring 5.0.M2, Spring provides a functional web framework that allows you to create "controller"-like constructs programmatically.
What you would need to do in your case is create a proper RouterFunction for the URLs you need to handle, and then simply handle the request with the appropriate HandlerFunction.
Keep in mind however that these constructs are not part of Spring MVC, but part of Spring Reactive.
Check out this blog post for more details
I believe that simple example is worth more than 1000 words :) In SpringBoot it will look like...
Define your controller (example health endpoint):
public class HealthController extends AbstractController {
#Override
protected ModelAndView handleRequestInternal(#NotNull HttpServletRequest request, #NotNull HttpServletResponse response) {
return new ModelAndView(new MappingJacksonSingleView(), "health", Health.up().build());
}
}
And create your configuration:
#Configuration
public class CustomHealthConfig {
#Bean
public HealthController healthController() {
return new HealthController();
}
#Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(Integer.MAX_VALUE - 2);
mapping.setUrlMap(ImmutableMap.of("/health", healthController()));
return mapping;
}
}
Regarding the handler order - take a look about the reason here: Java configuration of SimpleUrlHandlerMapping (Spring boot)
The MappingJacksonSingleView is auxiliary class in order to return single json value for model and view:
public class MappingJacksonSingleView extends MappingJackson2JsonView {
#Override
#SuppressWarnings("unchecked")
protected Object filterModel(Map<String, Object> model) {
Object result = super.filterModel(model);
if (!(result instanceof Map)) {
return result;
}
Map map = (Map) result;
if (map.size() == 1) {
return map.values().toArray()[0];
}
return map;
}
}
Jackson single view source - this blog post: https://www.pascaldimassimo.com/2010/04/13/how-to-return-a-single-json-list-out-of-mappingjacksonjsonview/
Hope it helps!
It is possible (now) to register request mapping by RequestMappingHandlerMapping.registerMapping() method.
Example:
#Autowired
RequestMappingHandlerMapping requestMappingHandlerMapping;
public void register(MyController myController) throws Exception {
RequestMappingInfo mappingInfo = RequestMappingInfo.paths("xxxx/info").methods(RequestMethod.GET).build();
Method method = myController.getClass().getMethod("info");
requestMappingHandlerMapping.registerMapping(mappingInfo, myController, method);
}
You could register your own HandlerMapping by extending the RequestMappingHandlerMapping, e.g. override the registerHandlerMethod.
It's not quite clear what you're trying to achieve, but maybe you can use #PathVariable in your #RequestMapping, something like:
#RequestMapping("/affId/{id}")
public void myMethod(#PathVariable("id") String id) {}
Edit: Original example has changed it appears, but you might be able to use PathVariable anyway.

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