My interface:
package com.demo.dependency;
#RestController
#RequestMapping(value = "#{'${api.baseUrl}'}")
public interface BaseController<Response> {
#PostMapping(value = "#{'${api.interface}'}")
#ResponseBody
public Response process(#RequestBody #Valid Request request) throws Exception;
}
Implementation:
package com.demo.application;
public class BarController implements BaseController<BarResponse> {
#Override
public BarResponse process(Request request) throws Exception {
// do something
}
}
I'm new to Spring Boot. I wonder whether these annotations can work properly in implementation class:
#RestController and #RequestMapping on interface class
#PostMapping and #ResponseBody on interface method
#RequestBody and #Valid on method parameters
#{'${api.baseUrl}'} and #{'${api.interface}'} to read configure from application.properties
My spring boot version is 2.2.6.
It seems that my SpringApplication from package com.demo.application failed to auto scan BarController. However, this answer says that "the annotation should apply to all subclasses", including #Service. Is anything wrong with my code?
Thanks in advance.
Related
I have the following controller class in my Spring boot project, split into interface and implementation:
public interface UserAccountController {
#RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(#RequestBody UserAccountEntity account,
HttpServletResponse response) throws IOException;
#RequestMapping(value = "/signup", method = RequestMethod.POST)
public String create(#Valid #RequestBody UserAccountEntity userAccount,
HttpServletResponse response, BindingResult result);
}
#RestController
#RequestMapping("/api/authentication")
public class UserAccountControllerImpl implements UserAccountController {
#Autowired
private UserAccountService userAccountService;
#Override
public String login(#Valid #RequestBody UserAccountEntity account,
HttpServletResponse response) throws IOException {
//...
}
#Override
public String create(#Valid #RequestBody UserAccountEntity userAccount,
HttpServletResponse response, BindingResult result) {
//....
}
}
When I move RestController and RequestMapping annotations to the interface, it doesn't work. But annotating the methods on the interface work. How are these two annotations different?
#RestController inherits from #Controller, which inherits from #Component thus leading to the creation of a Spring Bean in your application context.
#RequestMapping inherits from #Mapper and is used to mark Rest- or Web-Controller methods as handler methods.
As to why Spring is implemented to disallow inheritance on the first and allow it on the second, I can only speculate:
I think your example constitutes a useful usecase for #Mapping inheritance, since you could have several RestControllers with different url prefixes, but other than that the same endpoints.
Making #Component annotations inheritable could lead to involuntarily created Spring Beans, since clients might fail to notice the annotation when implementing the interface.
HelloController.java
#RestController
class HelloController {
#GetMapping(value = "{id}/hello")
public ModelAndView listAPI(#PathVariable("id") String profileId) {
ModelAndView mav = new ModelAndView();
return mav;
}
}
HelloControllerTest.java
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#ContextConfiguration(classes = HelloConfigTest.class)
class HelloControllerTest {
#Inject
private WebApplicationContext webApplicationContext;
#Inject
private Foo mockFoo
#InjectMocks
HelloController helloController;
private MockMvc mockMvc;
#Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
#Test
public void testHello() throws Exception {
mockMvc.perform(
get("/{id}/campaigns", "id1"))
.andExpect(status().isOk()));
}
}
// I have another test that directly calls the controller method.
// So I need #InjectMocks to get an instance of the controller
#Test
public void test2() {
when(mockFoo.getX()).thenReturn(true);
helloController.saveAPI();
}
HelloConfigTest.java
#Configuration
#ComponentScan("com.test.controller")
class HelloConfigTest {
#Bean
public mockFoo() {
return Mockito.mock(Foo.class);
}
}
The response that I get here is 404 and I expect 200.
But it works and I get 200 if I change #GetMapping to #RequestMapping(value="{id}/hello", method=RequestMethod.GET)
Am I missing anything here ?
Your configuration is extremely bare bones
#Configuration
#ComponentScan("com.test.controller")
class HelloConfigTest {
It doesn't register any Spring MVC infrastructure beans, either implicitly or explicitly.
When MockMvc, internally, creates a TestDispatcherServlet to test your #Controller class, it has to defer to some default Spring MVC infrastructure types.
Among these infrastructure types is HandlerMapping which is
to be implemented by objects that define a mapping between requests and handler objects.
The default implementation used by the TestDispatcherSerlet is DefaultAnnotationHandlerMapping (an old class) which looks for #RequestMapping specifically, it doesn't recursively do a meta-annotation lookup. Your #GetMapping annotated method is therefore not found and not registered as a handler.
If, instead, you configure your application context with #EnableWebMvc
#Configuration
#ComponentScan("com.test.controller")
#EnableWebMvc
class HelloConfigTest {
Spring will implicitly register a RequestMappingHandlerMapping, which does do this "recursive" lookup for the annotation hierarchy (called merging). Since #GetMapping is annotated with #RequestMapping, the annotated handler method will be found and registered.
As for the #InjectMocks, note that the instance referenced by the field is different from the one used to handle the request performed by the MockMvc object. The former is managed by Mockito, the latter by Spring.
How to intercept #RestController methods using MethodInterceptor in SpringBoot?
Prior to using springboot, I have a simple Interceptor that logs the execution time of a bean method. Simply intercepts all spring bean methods through a default proxy definition.
#Component
public class MethodTimer implements MethodInterceptor {
#Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
final StopWatch stopWatch = new StopWatch();
stopWatch.start(methodInvocation.getMethod().toGenericString());
try {
return methodInvocation.proceed();
}
finally {
stopWatch.stop();
System.out.println(stopWatch.prettyPrint());
}
}
}
The spring configuration where com.mypackages contains the beans and the MethodInterceptor implementation.
<context:component-scan base-package="com.mypackages" />
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
<property name="proxyTargetClass" value="true"/>
</bean>
The configuration above is loaded via
#ContextConfiguration("/application-context.xml")
Method executions are logged, everything seems to be working, life is good.
Moving to SpringBoot
I use the same code, switched to annotations (eliminating the *.xml) configuration
#SpringBootApplication(scanBasePackages = { "com.mypackages" })
#EnableAutoConfiguration
#EnableAspectJAutoProxy(proxyTargetClass=true)
Added a constructor to the MethodTimer with System.out.println() to make sure that the Interceptor has been loaded by spring boot
public MethodTimer() {
System.out.println("MethodTimer - Constructor");
}
And yes, it was loaded and created as "MethodTimer - Constructor" is found in the console logs.
However, none of the #RestController methods are intercepted. Below is a simple Rest Controller
#RestController
#RequestMapping("/test")
public class HelloWorldService {
#RequestMapping(method = RequestMethod.GET)
public String sayHello() {
System.out.println("Hello World!");
return "Hello World!";
}
}
Even tried creating a pure spring bean via #Component and #Autowired it in the #RestController to see if that pure spring bean is intercepted, but it was not intercepted as well.
A simple Test service bean. The test() method should be intercepted.
#Component
public class TestService {
public void test() {
System.out.println("TestService.test()");
}
}
Revised RestController
#RestController
#RequestMapping("/test")
public class HelloWorldService {
#Autowired
private TestService testService;
#RequestMapping(method = RequestMethod.GET)
public String sayHello() {
testService.test();
return "Hello World!";
}
}
Notes
The MethodTimer was loaded as the constructor was called showing the System.out.println log in the console, it seems however spring boot did not automatically determine that this bean/component is implementing MethodInterceptor.
The simple Spring Bean annotated with #Component bean was not intercepted.
The Rest Controller annotated with #RestController was not intercepted.
I have tried adding #Component, #Service in the RestController, it did not work.
I have tried tried #EnableAutoConfiguration, adding spring.aop.* configurations in the application.properties, did not work.
Tried using SpringBoot version 1.5.4.RELEASE, does not work.
I know I can always try to use Aspect as shown in the spring-boot-sample-aop example: https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples/spring-boot-sample-aop
But not able to use previous spring code and configure it with springboot is just too lame. If MethodInterceptor and DefaultAdvisorAutoProxyCreator is not supported anymore in spring boot, it should have been deprecated
I'm having some problem with Spring MVC 4.2.6 and Jackson (2.7.3). I created an absctract #RestController class with a method that uses a generic type. I implement this abstract class passing the type of generic object. I use #RequestBody and the DispatcherServlet throws JsonMappingException (can not construct instance of my class). The MappingJackson2HttpMessageConverter in application context is configured.
Edit: More info added.
public abstract class AbsctractAnimalResource<DTO extends AnimalDTO> {
// Doesn't work. Throws JsonMappingException. (#RequestBody)
#RequestMapping(value = "/bar", method = POST, produces = APPLICATION_JSON_VALUE)
public ResponseEntity<?> bar(#RequestBody DTO dto) throws Exception {
return new ResponseEntity(dto, HttpStatus.CREATED);
}
}
#RestController
#RequestMapping("/cat")
public class CatResource extends AbsctractAnimalResource<CatDTO> {
}
#RestController
#RequestMapping("/dog")
public class DogResource extends AbsctractAnimalResource<DogDTO> {
}
interface AnimalDTO { }
public class CatDTO implements AnimalDTO { }
public class DogDTO implements AnimalDTO { }
After some debugs in Spring 4.2.6 classes I found this commentary:
https://github.com/spring-projects/spring-framework/blob/4.2.x/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java#L51
Spring 4.2.X and Jackson 2.7.X don't work together. I downgraded my Jackson version to 2.6.6 and now everything works. Spring 4.3.X works well with Jackson 2.7+.
I have a Spring Java configuration and I'd like to build 2 instances of the same controller
#Bean(name = CONTROLLER_A)
public MyController getMyAController() {
return new MyController(new A());
}
#Bean(name = CONTROLLER_B)
public MyController getMyBController() {
return new MyController(new B());
}
public class MyController {
...
#RequestMapping(method = RequestMethod.GET)
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response){
...
}
}
There is no way to annotate the methods as #Controller and without it, Spring doesn't consider that instance as Controller instances (the exception handling doesn't work properly).
Is there a way to get the controller work properly w/o using xml configuration?
EDIT: the only way I can make it works is by extending AbstractController, but I don't really want to use inheritance.