I have a problem with my test Spring Boot app. It works just fine, but when I enable Spring validation by adding dependency etc and adding a #Configuration:
#Configuration
public class TestConfiguration {
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
I get 404 for my test endpoint.
{
"timestamp": 1601507037178,
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/test"
}
I've already applied some solutions/proposals from similar problems (eg here or here) but without a success.
Here is my code:
https://github.com/zolv/error-handling-test
API interface:
#Validated
public interface TestApi {
#PostMapping(
value = "/test",
produces = {"application/json"},
consumes = {"application/json"})
#ResponseBody
ResponseEntity<TestEntity> getTest(#Valid #RequestBody(required = false) TestEntity request);
}
TestEntity just to send something:
#Data
public class TestEntity {
#JsonProperty("test")
#NotNull
private String test;
}
Controller implementation:
#RestController
#RequiredArgsConstructor
#Validated
public class TestController implements TestApi {
#Override
#ResponseBody
public ResponseEntity<TestEntity> getTest(#Valid #RequestBody TestEntity request) {
return ResponseEntity.ok(request);
}
}
My controller advice:
#ControllerAdvice
public class DefaultErrorHandlerAdvice extends ResponseEntityExceptionHandler {
#ExceptionHandler(value = {ConstraintViolationException.class})
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
#ResponseBody
public ResponseEntity<String> handleValidationFailure(ConstraintViolationException ex) {
StringBuilder messages = new StringBuilder();
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
messages.append(violation.getMessage());
}
return ResponseEntity.badRequest().body(messages.toString());
}
#Override
#ResponseBody
#ResponseStatus(HttpStatus.BAD_REQUEST)
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.body("problem");
}
}
Application:
#SpringBootApplication
#EnableWebMvc
#ComponentScan(basePackages = "com.test")
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
A test I use, but it fails also using Postman:
#SpringJUnitConfig
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TestControllerTest {
#Autowired protected TestRestTemplate restTemplate;
#Test
void registrationHappyPath() throws Exception {
/*
* Given
*/
final TestEntity request = new TestEntity();
/*
* When/
*/
final ResponseEntity<String> response =
restTemplate.postForEntity("/test", request, String.class);
/*
* Then
*/
Assertions.assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
final String body = response.getBody();
Assertions.assertNotNull(body);
}
}
If I comment out a TestConfiguration then everything works fine.
Thank You in advance for any help.
You should set MethodValidationPostProcessor#setProxyTargetClass(true) because by default MethodValidationPostProcessor uses JDK proxy which leads to loss of your controller in the Spring context.
When AbstractHandlerMethodMapping#processCandidateBean is called isHandler(Class<?> beanType) will return false because JDK proxy doesn't contain #RestController annotation.
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor mvProcessor = new MethodValidationPostProcessor();
mvProcessor.setProxyTargetClass(true);
return mvProcessor;
}
I have a couple of APIs and using springfox-swagger for API documentation.
I have a requirement to add the creation date to the respective API. How can I achieve this using swagger. I don't need any API versioning.
Ex:
#ApiOperation(value = "Creates a new user and returns the created user.")
#PostMapping(/user)
public ResponseEntity<UserDto> createUser(#RequestBody UserDto userDto) {
User user =userService.create(userDto);
return new ResponseEntity<>(UserMappers.USER_ENTITY_TO_DTO.apply(user),HttpStatus.CREATED);
}
In the above example, I want to add the creation date of /user so that I can trace the creation date.
In my project I have a similar requirement. As a solution I have created a custom annotation (for marking the endpoint) and wrote a plugin (for updating the API description).
Option #1
#ApiSince annotation:
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
public #interface ApiSince {
String value() default "";
}
ApiSincePlugin plugin:
#Component
public class ApiSincePlugin implements OperationBuilderPlugin {
private final DescriptionResolver resolver;
#Autowired
public ApiSincePlugin(DescriptionResolver resolver) {
this.resolver = resolver;
}
#Override
public void apply(OperationContext context) {
final String sinceTemplate = "### Since %s%n%n%s";
String notes = "";
Optional<ApiOperation> apiOperationOptional = context.findAnnotation(ApiOperation.class);
if (apiOperationOptional.isPresent()) {
notes = apiOperationOptional.get().notes();
}
String finalNotes = notes;
Optional<ApiSince> apiSinceOptional = context.findAnnotation(ApiSince.class);
if (apiSinceOptional.isPresent()) {
finalNotes = String.format(sinceTemplate, apiSinceOptional.get().value(), notes);
}
context.operationBuilder().notes(resolver.resolve(finalNotes));
}
#Override
public boolean supports(DocumentationType type) {
return true;
}
}
#ApiSince in action:
#ApiSince(value = "2019-10-31")
#PostMapping(value = "/login")
#ApiOperation(value = "Authenticate user", nickname = "login", notes = "your API description")
#ResponseStatus(HttpStatus.OK)
#ApiResponses(value = {
#ApiResponse(code = 200, response = LoginResponse.class, message = HTTP_200_OK),
...
})
#ResponseBody
ResponseEntity<LoginResponse> login(...);
If you don't want do add it in the description but as an extra JSON attribute then take a look at this solution: Custom Operation Builder Plugin
.
Option #2
#ApiSince annotation (code same as above)
ApiSincePlugin plugin:
#Component
public class ApiSincePlugin implements OperationBuilderPlugin {
#Override
public void apply(OperationContext context) {
Optional<ApiSince> annotation = context.findAnnotation(ApiSince.class);
if (annotation.isPresent()) {
String value = annotation.get().value();
ObjectVendorExtension extention = new ObjectVendorExtension("x-since");
extention.addProperty(new StringVendorExtension("value", value));
context.operationBuilder().extensions(Collections.singletonList(extention));
}
}
#Override
public boolean supports(DocumentationType documentationType) {
return true;
}
}
Activate extensions in the Swagger UI:
#Bean
UiConfiguration uiConfig() {
return UiConfigurationBuilder
.builder()
.showExtensions(true)
...
.build();
}
#ApiSince in action (code same as above):
I am new to Jersey and trying to convert a project from Spring MVC into Jersey. With my current build however, all requests return a resource not available error. Any help would be greatly appreciated.
My Dependencies:
dependencies {
compile('org.springframework.boot:spring-boot-starter')
compile("org.springframework.boot:spring-boot-starter-data-jpa:1.4.0.RELEASE")
compile("org.springframework.boot:spring-boot-starter-jersey")
runtime('org.hsqldb:hsqldb')
compileOnly("org.springframework.boot:spring-boot-starter-tomcat")
testCompile('org.springframework.boot:spring-boot-starter-test')
}
My Jersey Config
#Configuration
public class JersyConfig extends ResourceConfig {
public JersyConfig() {
registerEndpoints();
configureSwagger();
}
private void configureSwagger() {
register(ApiListingResource.class);
BeanConfig beanConfig = new BeanConfig();
beanConfig.setVersion("1.0.0");
beanConfig.setSchemes(new String[]{"http"});
beanConfig.setHost("localhost:8090");
beanConfig.setBasePath("/");
beanConfig.setResourcePackage(OwnerController.class.getPackage().getName());
beanConfig.setPrettyPrint(true);
beanConfig.setScan(true);
}
private void registerEndpoints() {
register(OwnerController.class);
}
}
#Api(value = "Owner controller", tags = {"Owner resource"})
public class OwnerController {
private final ClinicService clinicService;
#Autowired
public OwnerController(ClinicService clinicService) {
this.clinicService = clinicService;
}
#GET
#Path("/{ownerId}")
#Produces(MediaType.APPLICATION_JSON)
#ApiOperation(value = "get owner by id", response = Owner.class)
public Response getOwner(
#ApiParam(name = "owner id", value = "owner id that must be fetched") #PathParam("ownerId") int id ) {
Owner owner = clinicService.findOwnerById(id);
return Response.status(200).entity(owner).build();
}
#GET
#Path("/owners")
#Produces(MediaType.APPLICATION_JSON)
#ApiOperation(value = "get all owners", response = Owner.class, responseContainer = "List")
public Response getOwners() {
List<Owner> owner = (List<Owner>) clinicService.findAllOwners();
return Response.status(200).entity(owner).build();
}
}
Register your package which contains jersey resources using packages() method in JerseryConfig() constructor -
public JersyConfig() {
packages("PACKAGE_CONTAINING_JERSEY_RESOURCES");
registerEndpoints();
configureSwagger();
}
Here is my Rest Service Class
#Controller
#RequestMapping(value = "/system_users")
#Secured({ "ROLE_MANAGER", "ROLE_EDITOR" })
public class SystemUsers {
#Inject
SystemUserBo systemUserBo;
...
...
...
#RequestMapping(value = "/get_users", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE })
#ResponseBody
public RestResponse getUser(#RequestBody String userName) {
try {
String[] systemUser = systemUserBo.getRecord(userName);
return new RestResponse(systemUser);
} catch (Throwable t) {
return new RestResponse(RestResponse.ERR_UNKNOWN);
}
}
...
...
...
}
I secured other methods with ROLE_MANAGER, ROLE_EDITOR and their specific tags
and i want to give full access only that method.
Which annotation should i use?
Thanks for helping.
I have very strange problem. In simple project I used Spring-Boot with oAuth2 (it is exactly jhipster generated project).
In services I connect with remote controllers (remote API) by restTemplate class. And I created special class to store cookieSession access to this remote API (this class has Session scope).
During authorization I save cookieSession from remote API to Session Scope class, and then when I make request to other part of remote API I use this seesionCookie.
Problem is, when I make asynchronous requesting from AngulrJS then sometimes Session scope class exist and sometimes it doesn't have data (is empty), but when I refresh website I have this data (without making next authorization). Whan I make synchronous requests there is no problem.
#Service
#Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class AuthorizationOsipDataService implements Serializable {
private String cookieSession;
public String getCookieSession() {
return cookieSession;
}
public void setCookieSession(String cookieSession) {
this.cookieSession = cookieSession;
}
}
Service:
#Service
public class OsipService {
#Autowired
private RestTemplate restTemplate;
#Autowired
private AuthorizationOsipDataService authorizationOsipDataService;
public String signInToOsipAndGetCookieSession (String login, String password) throws SignInToOsipException {
MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
map.add("j_username", login);
map.add("j_password", password);
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<MultiValueMap<String, String>>(map, new HttpHeaders());
log.debug("Logging... user: '{}'", login);
ResponseEntity response = restTemplate.exchange(osipUrl + authorizationUrl, HttpMethod.POST, requestEntity, String.class);
if(isLogged(response)){
String cookieSession = response.getHeaders().getFirst(HttpHeaders.SET_COOKIE);
log.debug("Succes login, setting authorizationOsipDataService");
authorizationOsipDataService.setPassword(password);
authorizationOsipDataService.setUsername(login);
authorizationOsipDataService.setCookieSession(cookieSession);
selectCompanyContext("538880bde511f776304687e6");
if(hasRoleOsipLite().getBody()){
return cookieSession;
} else {
throw new SignInToOsipException("User doesn't has ROLE_OSIPLITE");
}
} else{
throw new SignInToOsipException("Login error, HttpSatus:"+ response.getStatusCode().toString());
}
}
private boolean isLogged(ResponseEntity response){
//if location contains '/signin', it means that there is redirect and signin is failed
return !response.getHeaders().getFirst(HttpHeaders.LOCATION).contains("osip/signin");
}
public ResponseEntity selectCompanyContext(String companyContextId){
HttpHeaders httpHeaders = makeHeadersWithJson();
HttpEntity<String> requestEntity = new HttpEntity<String>(httpHeaders);
log.debug("Selecting context... '{}' ", companyContextId);
return restTemplate.exchange(osipUrl + selectCompanyContextUrl + companyContextId, HttpMethod.GET, requestEntity, String.class);
}
public ResponseEntity<NipExistDTO> isExistNip(String nip){
HttpHeaders httpHeaders = makeHeadersWithJson();
HttpEntity<String> requestEntity = new HttpEntity<String>(httpHeaders);
log.debug("isExistTest for nip: '{}'", nip);
return restTemplate.exchange(osipUrl + existNipUrl + nip, HttpMethod.GET, requestEntity, NipExistDTO.class);
}
}
...
...
...
Controllers:
#RestController
#RequestMapping("/customer")
public class CustomerResource {
private final Logger log = LoggerFactory.getLogger(CustomerResource.class);
#Autowired
private OsipService osipService;
#RequestMapping(value = "nipExist", method = RequestMethod.GET)
public
#ResponseBody
ResponseEntity<NipExistDTO> isNipExist(#RequestParam String nip) throws SignInToOsipException {
return osipService.isExistNip(nip);
}
#RequestMapping(value = "add", method = RequestMethod.POST)
public
#ResponseBody
ResponseEntity addCustomer(#RequestBody NewCustomerDTO newCustomerDTO) throws SignInToOsipException {
return osipService.addCustomerToOsip(newCustomerDTO);
}
}
WebConfig (configuration of Session Scope)
public void onStartup(ServletContext servletContext) throws ServletException {
log.info("Web application configuration, using profiles: {}", Arrays.toString(env.getActiveProfiles()));
EnumSet<DispatcherType> disps = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ASYNC);
if (!env.acceptsProfiles(Constants.SPRING_PROFILE_FAST)) {
initMetrics(servletContext, disps);
}
if (env.acceptsProfiles(Constants.SPRING_PROFILE_PRODUCTION)) {
initCachingHttpHeadersFilter(servletContext, disps);
initStaticResourcesProductionFilter(servletContext, disps);
initGzipFilter(servletContext, disps);
}
log.info("Web application fully configured");
servletContext.addListener(new RequestContextListener());
}
AngularJS
angular.module('osipliteApp')
.controller('CustomerController', function ($rootScope, $scope, Upload, $timeout,Customer,Scenario,Dictionary,$loading,$state,Auth) {
$loading.start('addCustomer');
$scope.isCollapsed=true;
//**** Initializing fields ****//
$scope.customerDTO = {name: null, nip: null, street: null,streetNumber:null, postOffice:null, zipCode:null, phoneNumber1: null, surveyNotes:null};
$scope.personEditDTO = {name: null, email:null,code1:null, phone1:null};
$scope.newCustomerDTO = {customerType: null, scenarioId:null};
$scope.personEditDTO.code1= '+48';
$scope.customerTypes = [{name:"Osoba fizyczna",value:"NATURAL_PERSON"},{name:"Jednostka budżetowa",value:"BUDGETARY_UNITS"},{name:"Spółka prawa handlowego",value:"COMMERCIAL"},{name:"Osoba fizyczna prowadząca działalność gospodarczą",value:"NATURAL_PERSON_WITH_BUSINESS"}];
$scope.products = Dictionary.get({dictionaryCode: 'PRODUCT_TYPE',languageCode:"PL"},function(success){
$scope.scenariosList = Scenario.get({value:'active'},function(success){$loading.finish('addCustomer');},function(error){restErrorHandler(error);});
},function(error){restErrorHandler(error);});
$scope.clear = function () {
$scope.customerDTO = {name: null, nip: null, street: null,streetNumber:null, postOffice:null, zipCode:null, phoneNumber1: null, surveyNotes:null};
$scope.personEditDTO = {name: null, email:null,code1:"+48", phone1:null};
$scope.newCustomerDTO = {customerType: "NATURAL_PERSON", scenarioId:null};
$scope.nipInvalid = null;
$scope.nipExist = null;
clearSurvey();
};
...
...