I have a complex bean holding the rest parameters, eg:
public class MyRestParams {
private HttpServletRequest req;
private #NotBlank String name;
//getter, setter
}
Usage:
#RestController
#RequestMapping("/xml")
public class MyServlet {
#RequestMapping(value = "/")
public void getTest(#Valid MyRestParams p) {
Sysout(p.getName()); //works when invoked with /xml?name=test
Sysout(p.getReq()); //always null
}
}
Problem: the HttpServletRequest is always null. Isn't it possible to add this parameter within the bean itself?
You can provide an implementation for HandlerMethodArgumentResolver to resolve your MyRestParams:
public class MyRestParamsArgumentResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(MyRestParams.class);
}
#Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
MyRestParams restParam = new MyRestParams();
restParam.setReq((HttpServletRequest) webRequest.getNativeRequest());
return restParam;
}
}
Then register it in your WebMvcConfigurerAdapter:
#EnableWebMvc
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new MyRestParamsArgumentResolver());
}
}
When using that form of method signature Spring will use your bean as a model attribute. Spring will bind your request parameters to bean properties of matching names using a WebDataBinder e.g ServletRequestDataBinder.
Since there is no request parameter which matches your bean property req the field will never be set. Even if the request parameter with name req existed in your request it wont be convertible to a HttpServletRequest.
To receive the actual request add a parameter of type HttpServletRequest to your handler method
#RequestMapping(value = "/")
public void getTest(#Valid MyRestParams p , HttpServletRequest request) {
Sysout(p.getName()); //works when invoked with /xml?name=test
Sysout(request); //always null
}
Or a parameter of type WebRequest if you dont want to tie yourself to the Servlet API.
Related
I am using spring argument resolver by implementing HandlerMethodArgumentResolver. It is working fine and setting parameters which is coming from header, but it's preventing parameter which came from request param/path variable or request body.
my code has requirement of both like some params are coming from header while other from request body or path. below is my code,
config class:
#Configuration
#EnableWebMvc
public class SpringWebMvcConfig implements WebMvcConfigurer {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new SpringArgumentResolver());
}
}
resolver class:
public final class SpringArgumentResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return CommonHeader.class.isAssignableFrom(parameter.getParameterType());
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
CommonHeader commonHeader = BeanUtils.instantiateClass(parameter.getParameterType(), CommonHeader.class);
String request_id = webRequest.getHeader("request_id");
if (!Strings.isNullOrEmpty(request_id)) {
commonHeader.setRequest_id(request_id);
}
return commonHeader;
}
}
commonHeader class:
#Getter
#Setter
public class CommonHeader {
private String request_id;
private String ip_address;
}
request class:
#Getter
#Setter
public class GetDataRequest extends CommonHeader {
private String user_id;
private String userName;
}
controller method:
#GetMapping("user_data")
public DeferredResult<ResponseEntity<JsonNode>> getUserData(GetDataRequest getDataRequest) {
DeferredResult<ResponseEntity<JsonNode>> deferredResult = new DeferredResult<>();
// some logic
return deferredResult;
}
this code is working, not it is either setting parameters which is coming in header, or only parameters which comes in as request params. some problem with argument resolver. don't know what. because I have this type of argument resolver working fine with spring webflux project. can anyone help me with this issue?
I have a custom implementation of the Picketlink PathAuthorizer interface that checks if a URL is allowed for the user.
public class BssPathAuthorizer implements PathAuthorizer {
#Inject
Identity identity;
#Override
public boolean authorize(PathConfiguration pathConfiguration,
HttpServletRequest request,
HttpServletResponse response) {
if (identity != null){
LOG.log(Level.FINE, "Identity loggato: {0}", identity.isLoggedIn());
String uri = request.getRequestURI();
String contextpath = request.getContextPath();
LOG.log(Level.FINE, "URI: {0}, context path: {1}",
new Object[]{uri, contextpath});
Method m = findMethod(uri);
...
}
After I get the method by findMethod(), I'll check some annotations and then return true if the user has permission.
Is there a simple way to retrieve the Java method from the requested URL (for example: .../user/edit)?
What is the class method that implement it (for example UserManager.edit())?
The information you need from JAX-RS is available in the ResourceInfo interface.
See below how to make this information available in your Picketlink PathAuthorizer implementation.
Defining a class to store the data you need
Define a class annotated with #RequestScoped which will store the target class and method:
#RequestScoped
public class RequestTarget {
private Class<?> targetClass;
private Method targetMethod;
// Default constructor, getters and setters ommited
}
Ensure you are using the #RequestScoped annotation from the javax.enterprise.context package.
Creating a request filter
Create a ContainerRequestFilter to populate the RequestTarget:
#Provider
#Priority(1)
public class RequestTargetPopulator implements ContainerRequestFilter {
#Context
private ResourceInfo resourceInfo;
#Inject
private RequestTarget target;
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
target.setTargetClass(resourceInfo.getResourceClass());
target.setTargetMethod(resourceInfo.getResourceMethod());
}
}
The #Priority annotation with the value 1 ensures this filter will be executed before other filters.
Performing the injection
And then you can finally perform the injection of the RequestTarget using #Inject:
public class CustomPathAuthorizer implements PathAuthorizer {
#Inject
private RequestTarget target;
#Override
public boolean authorize(PathConfiguration pathConfiguration,
HttpServletRequest request,
HttpServletResponse response) {
Class<?> targetClass = target.getTargetClass();
Method targetMethod = target.getTargetMethod();
...
}
}
I want to check if user exists in ControllerAdvice and treat user as #ModelAttribute if user exists. On the other hand, I also want to access user object in #Controller directly. So I add #ModelAttribute annotation on the parameter of #RequestMapping method.
I'm using #ControllerAdvice like:
#ControllerAdvice
public class UserAdvice {
#Autowired
private UserService userService;
#ModelAttribute("user")
public User user(#PathVariable("username") String username) {
User user = userService.findByUsername(username);
if (user != null) {
return user;
}
user = userService.findById(username);
if (user == null) {
throw new ResourceNotFoundException("user not found");
}
return user;
}
}
And UserController Like:
#RestController
#RequestMapping("/users/{username}")
public class UserController {
public static final Logger logger = LoggerFactory.getLogger(UserCourseListController.class);
#Autowired
private CourseService courseService;
#RequestMapping(value = "", method = RequestMethod.GET)
public void getUser(#ModelAttribute("user") User user, Model model) {
logger.info("{}", user);//user is null
logger.info("{}", model.asMap().get("user"));// not null
}
}
But now, the parameter user that annotated with #ModelAttribute is null while there is a "user" obj in Model Map.
Is there any mistakes I've made in this scenario? Or any misunderstanding of the concepts of #ModelAttribute and #ControllerAdvice?
Thanks very much!
Update
From Docs of Springframework:
Once present in the model, the argument’s fields should be populated from all request parameters that have matching names.
So We cannot add #ModelAttribute to method parameters annotated by #RequestMapping directly because Spring will do data binding from request(not Model)。
Finally I found a solution——HandlerMethodArgumentResolver. It can resolve method arguments on each #RequestMapping method and do some work on resolving arguments. An example of Java Config is below:
public class Config extends WebMvcConfigurerAdapter {
#Bean(name = "auditorBean")
public AuditorAware<User> auditorAwareBean() {
return () -> null;
}
#Bean
public HttpMessageConverters customConverters() {
return new HttpMessageConverters(new MappingJackson2HttpMessageConverter());
}
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new HandlerMethodArgumentResolver() {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(User.class);
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return mavContainer.getDefaultModel().get(parameter.getParameterName());
}
});
}
}
We resolve method arguments from model via parameter.getParameterName(). It mean that the name of method argument(user) must be equal to the value of #ModelAttrubute defined in #ControllerAdvice. You can also use any other naming conventions to implement the binding.
I have Spring rest controller that provides operations on Project entity. All methods use same entity accessing code. I don't want to copy&paste #PathVariable parameters in all methods, so I've made something like this.
#RestController
#RequestMapping("/projects/{userName}/{projectName}")
public class ProjectController {
#Autowired
ProjectService projectService;
#Autowired
protected HttpServletRequest context;
protected Project project() {
// get {userName} and {projectName} path variables from request string
String[] split = context.getPathInfo().split("/");
return projectService.getProject(split[2], split[3]);
}
#RequestMapping(method = GET)
public Project get() {
return project();
}
#RequestMapping(method = GET, value = "/doSomething")
public void doSomething() {
Project project = project();
// do something with project
}
// more #RequestMapping methods using project()
}
Is it possible to autowire path variables into controller by annotation so I don't have to split request path and get parts of it from request string for project() method?
In order to do custom binding from request you've got to implement your own HandlerMethodArgumentResolver (it's a trivial example without checking if path variables actually exist and it's also global, so every time you will try to bind to Project class this argument resolver will be used):
class ProjectArgumentResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterType().equals(Project.class);
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
Map<String, String> uriTemplateVars = (Map<String, String>) webRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
return getProject(uriTemplateVars.get("userName"), uriTemplateVars.get("projectName"));
}
private Project getProject(String userName, String projectName) {
// replace with your custom Project loading logic
Project project = new Project(userName, projectName);
return project;
}
}
and register it using WebMvcConfigurerAdapter:
#Component
public class CustomWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new ProjectArgumentResolver());
}
}
In your controller you have to put Project as a method argument, but do not annotate it with #PathVariable:
#Controller
#RequestMapping("/projects/{userName}/{projectName}")
public class HomeController {
#RequestMapping(method = RequestMethod.GET)
public void index(Project project){
// do something
}
}
Spring 3.2. Everything works when I do:
#Controller
public class MyController {
#Inject
Provider<MyBean> provider;
#RequestMapping("/chart")
public void getChart(HttpServletResponse resp) {
provider.get();
}
}
but it doesn't work when I set MyBean as an argument to getChart:
#Controller
public class MyController {
#RequestMapping("/chart")
public void getChart(HttpServletResponse resp, MyBean myBean) {
// No such method MyBean.<init>()
}
}
So Spring tries to create a new instance of myBean instead of using already bound one.
Configuration:
#Configuration
public class Config {
#Inject
#Bean #Scope("request")
public MyBean provideMyBean(MyOtherBean myOtherBean) {
return myOtherBean.getMyBean();
}
}
It also doesn't work if I make my controller request scoped, and add #Inject/#Autowired to the getChart(). Then it cannot find HttpServletResponse instance (NoSuchBeanDefinitionException), although there surely must be one in request-scope.
Maybe it just isn't implemented in Spring?
Resolved by creating a custom HandlerMethodArgumentResolver:
/**
* Resolves beans defined in {#link Config},
* because Spring doesn't want to do it implicitly.
*
* Makes possible to write something like
* #RequestMapping(value="/chart", method=RequestMethod.GET)
* getChart(HttpServletRequest req, MyBean myBean)
*
* and Spring will inject both arguments.
*/
public class MethodArgumentResolver implements HandlerMethodArgumentResolver, BeanFactoryAware {
private final Set<Class> knownTypes = Config.getDeclaredTypes();
private BeanFactory beanFactory;
#Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> type = parameter.getParameterType();
return knownTypes.contains(type);
}
#Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
Class<?> type = parameter.getParameterType();
return beanFactory.getBean(type);
}
}
and in Config:
static Set<Class> getDeclaredTypes() {
Set<Class> result = Sets.newHashSet();
Method[] methods = Config.class.getDeclaredMethods();
for (Method method : methods) {
if (method.getAnnotation(Bean.class) != null) {
result.add(method.getReturnType());
}
}
return result;
}