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.
Related
#RestController()
#RequestMapping(path = "/users")
public class UserController {
#GetMapping()
public #ResponseBody Page<User> getAllUsers(#RequestParam Integer pageSize, UserRequest userRequest) {
//TODO: some implementation
}}
public class UserRequest{
public String name;
public String age;
}
send the request with invalid parameter, like localhost:8800/users?name1=1234, I want to return error. but in fact it ignore the invalid parameter name1.
I tried to add the user defined annotation on the method parameter and on the class , codes like below
#RestController()
#RequestMapping(path = "/users")
#Validated
public class UserController {
#GetMapping()
public #ResponseBody Page<User> getAllUsers(#RequestParam #Validated Integer pageSize, #Validated UserRequest userRequest} {
//TODO: some implementation
}
}
But it does not working.
I think it is happened because framework has ignore the invalid parameter before the method was called.
where did framework handle the url and how can I do to make it return error instead of ignore?
You can reject parameters that are not valid. You can do so in a HandlerInterceptor class.
Reference: Rejecting GET requests with additional query params
In addition to what is done in the above reference, in your addInterceptors, you can specify the path that is intercepted.
Like this:
#Configuration
public class WebConfig implements WebMvcConfigurer {
private String USERS_PATH = "/users";
// If you want to cover all endpoints in your controller
// private String USERS_PATH = List.of("/users", "/users/**");
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new FooHandlerInterceptor()).addPathPatterns(USERS_PATH);
}
}
Let's assume we have a User entity with name property and a REST API which provides querying of the user data with pagination, sorting and filter on entity fields.
Can Spring MVC inject Example<User> from spring-data into controller method invocation as it does with Pageable ?
If so. How to make it work ?
In code:
To implement GET /bike?offset=20&name=Jo request which returns paginable results of all users whose name starts with "Jo" I use:
#GetMapping
public Page<User> all(Pageable p, User u) {
Example<User> ex = Example.of(u, startMatcher);
return repo.findAll(ex, p);
}
This contains unnecessary boilerplate.
Can I somehow achieve:
#GetMapping
public Page<Bike> all(Pageable p, Example<Bike> b) {
return repo.findAll(ex, p);
}
with custom set default ExampleMatcher ?
I ended up with custom web binding.
Resolver
public class FilterArgResolver implements HandlerMethodArgumentResolver {
ExampleMatcher startMatcher = ExampleMatcher.matching()
.withStringMatcher(ExampleMatcher.StringMatcher.STARTING);
#Override
public boolean supportsParameter(MethodParameter param) {
return param.getParameterAnnotation(Filter.class) != null;
}
#Nullable
#Override
public Object resolveArgument(MethodParameter param,
#Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
#Nullable WebDataBinderFactory binderFactory) throws Exception {
Class clazz = param.getParameterAnnotation(Filter.class).value();
Object o = clazz.getConstructor().newInstance();
ServletRequestDataBinder b = (ServletRequestDataBinder) binderFactory.
createBinder(webRequest, o, "filter");
b.bind((ServletRequest) webRequest.getNativeRequest());
BindingResult r = b.getBindingResult();
Object t = r.getTarget();
return Example.of(t, startMatcher);
}
}
Registration
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new FilterArgResolver());
}
}
Custom annotation
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.PARAMETER)
public #interface Filter {
Class<?> value();
}
Usage
#GetMapping
public Page<Bike> all(Pageable p, #Filter(Bike.class) Example<Bike> ex) {
return repo.findAll(ex, p);
}
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.
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
}
}
Dao
#Repository
public interface LoginDao extends JpaRepository<Login, Integer> {
Login findByLogin(String login);
}
Validator
#Component
public class PasswordChangeValidator implements Validator {
private LoginDao loginDao;
#Override
public boolean supports(Class<?> aClass) {
return PasswordChange.class.equals(aClass);
}
#Override
public void validate(Object o, Errors errors) {
PasswordChange passwordChange = (PasswordChange) o;
**// There is a null pointer here because loginDao is null**
Login login = loginDao.findByLogin(passwordChange.getLoginKey());
}
public LoginDao getLoginDao() {
return loginDao;
}
#Autowired
public void setLoginDao(LoginDao loginDao) {
**// There is a debug point on the next line and it's hit on server startup and I can
// see the parameter us non-null**
this.loginDao = loginDao;
}
}
Controller
#Controller
#RequestMapping("api")
public class PasswordController {
#Autowired
PasswordService passwordService;
#InitBinder("passwordChange")
public void initBinder(WebDataBinder webDataBinder, WebRequest webRequest) {
webDataBinder.setValidator(new PasswordChangeValidator());
}
#RequestMapping(value = "/passwordChange", method = RequestMethod.POST)
public #ResponseBody PasswordInfo passwordInfo(#RequestBody #Valid PasswordChange passwordChange)
throws PasswordChangeException {
return passwordService.changePassword(passwordChange.getLoginKey(), passwordChange.getOldPassword(), passwordChange.getNewPassword());
}
}
I have the Dao listed above. This same dao bean gets injected in an #Service annotated class but not in #Component annotated Validator class. Well, not exactly the upon server startup I can see that the setter method gets called, but when I try to use this variable in a method the variable shows as null.
Does anybody see a problem with my configuration ? Please note that the loginDao bean gets injected into a service class, so the Context configuration is good.
Well there's your problem
webDataBinder.setValidator(new PasswordChangeValidator());
Spring can only manage beans it created. Here, you're creating the instance. Instead inject your bean into the #Controller and use it.
#Inject
private PasswordChangeValidator passwordChangeValidator;
...
webDataBinder.setValidator(passwordChangeValidator);