Doing More With Swagger and Spring
Learn more about Spring and Swagger using the "spring-swagger-simplified" artifact.
Join the DZone community and get the full member experience.
Join For FreeIn my last article, I gave a quick introduction to Swagger in the Spring realm. Also, we saw how the additional Maven artifact "spring-swagger-simplified" automatically leverages the validation constraint annotations used by Spring and enriches the Swagger models and Swagger UI.
Below, for a quick recap, we look at the automatic model enhancements amongst other things we had discussed in the previous article.
Without spring-swagger-simplified |
With spring-swagger-simplified |
In this article, we will go deeper into various Swagger and Spring topics to elaborate on the value provided by the additional "spring-swagger-simplified" jar. We will then understand how to use Swagger apiInfo
and manage global Spring exception handling with Swagger. Finally, we will see how the Maven artifact "spring-swagger-simplified" can be helpful in the global exception handling-related Swagger documentation.
Let's get started in the same way as last time.
We are going to refer to the following sources as starting points: Swagger 2 documentation for Spring REST API, Building a RESTful Web Service, and the previous article, Simplified Spring Swagger.
Prerequisites:
Java 8.x
- Maven 3.x
Steps
Let's repeat the steps from the last article (a reference to the completed code is also provided at the end of this article).
We need to apply two changes.
In pom.xml, please use the latest version 1.0.4 (use whatever is the latest) for spring-swagger-simplified.
<dependency>
<groupId>org.bitbucket.tek-nik</groupId>
<version>1.0.4</version>
<artifactId>spring-swagger-simplified</artifactId>
</dependency>
Also, we are going to enhance SwaggerConfig.java by adding the below bean.
@Bean
ApiInfo apiInfo()
{
return new ApiInfo("A sample spring swagger example",
"This project demonstrates some spring "
+ "and swagger concepts. It also demonstrates "
+ "some enhancements in spring swagger using "
+ "spring-swagger-simplified",
"1.0.4",
"Use as you like- some url here",
new Contact("Contact Name",
"https://bitbucket.org/tek-nik/simplified-swagger-examples/",
null),
"The Apache Software License, Version 2.0",
"http://www.apache.org/licenses/LICENSE-2.0.txt",
Collections.EMPTY_LIST
);
}
Also, we need to add these imports:
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import java.util.Collections;
Lets now use this bean.
Below, we have the complete modified SwaggerConfig.java with an additional reference to the above bean:
package sample;
import java.util.Collections;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.google.common.base.Predicates;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
ApiInfo apiInfo()
{
return new ApiInfo("A sample spring swagger example",
"This project demonstrates some spring "
+ "and swagger concepts. It also demonstrates "
+ "some enhancements in spring swagger using "
+ "spring-swagger-simplified",
"1.0.4",
"Use as you like- some url here",
new Contact("Contact Name",
"https://bitbucket.org/tek-nik/simplified-swagger-examples/",
null),
"The Apache Software License, Version 2.0",
"http://www.apache.org/licenses/LICENSE-2.0.txt",
Collections.EMPTY_LIST
);
}
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2).
select()
.apis(Predicates.not(RequestHandlerSelectors.
basePackage("org.springframework.boot")))
.paths(PathSelectors.any()).build()
.apiInfo(apiInfo());
}
}
-
As you can see, the apiInfo()
bean is being passed on to docket.apiInfo()
. [See line number 49 above]. Make sure you are doing the same else copy from the above SwaggerConfig.java
Let's rebuild, restart the application, and visit: http://localhost:8080/swagger-ui.html.
For now, let's comment out our dependency spring-swagger-simplified in the pom.xml using <! and -->
<!--
<dependency>
<groupId>org.bitbucket.tek-nik</groupId>
<version>1.0.4</version>
<artifactId>spring-swagger-simplified</artifactId>
</dependency>
-->
We will now discuss Global Exception handling using @ControllerAdvice
.
Let's start by modifying the PersonController
we had created last time.
package sample;
import java.util.Random;
import javax.validation.Valid;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PersonController {
private Random ran = new Random();
@RequestMapping(path = "/person", method = RequestMethod.POST)
public Person person(@Valid @RequestBody Person person) {
int nxt = ran.nextInt(10);
if(nxt==5)
{
throw new RuntimeException("Breaking logic");
}
return person;
}
}
Let's rebuild and rerun the application.
We are trying to simulate some complex logic that causes the controller to throw exceptions at times. Since we don't have complex real logic, we are using Random
and expecting the controller to fail at times. We can test this by repeatedly invoking the execution.
Before getting around to that, you might have noticed that the Swagger UI has reverted to its default by not showing full package names after we commented out our dependency "spring-swagger-simplified."
This can even confuse Swagger if two models have the same name but different packages. While this behavior can certainly be altered by writing more Swagger configuration code, it's one of the details already taken care of by the artifact — spring-swagger-simplified — the artifact which we have currently commented out. It doesn't affect us as of now because we do not, as of yet, have two models with the same class name in different packages.
Also, you might have noticed the model has been stripped of the extra information it was showing: no min max, etc.
Let's get back to invoking the controller and seeing what happens when it fails by invoking it repeatedly.
Let's fill up valid data (keep it handy say in notepad to copy from when needed):
{
"age": 18,
"creditCardNumber": "4111111111111111",
"email": "abc@abc.com",
"email1": "abc1@abc.com",
"firstName": "string",
"lastName": "string"
}
Now, press the execute button until we get an error response, as shown here:
This is all very nice. What if you wanted to send out your own representation of the message? What if you wanted to create a log reference id and convey it in response so that you can leverage the reported log reference id and correlate quickly with server back end logs on the server? Let's say we want to do all of this in a centralized manner.
That's where @ControllerAdvice
comes in. Time for some more code.
package sample;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
@ControllerAdvice @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public class GlobalControllerAdvice //extends ResponseEntityExceptionHandler
{
/**
* Note use base class if you wish to leverage its handling.
* Some code will need changing.
*/
private static final Logger logger = LoggerFactory.getLogger(GlobalControllerAdvice.class);
@ExceptionHandler(Throwable.class)
public ResponseEntity < Problem > problem(final Throwable e) {
String message =e.getMessage();
//might actually prefer to use a geeric mesasge
message="Problem occured";
UUID uuid = UUID.randomUUID();
String logRef=uuid.toString();
logger.error("logRef="+logRef, message, e);
return new ResponseEntity <Problem> (new Problem(logRef, message), HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorMessage> handleMethodArgumentNotValid(MethodArgumentNotValidException ex
) {
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
List<ObjectError> globalErrors = ex.getBindingResult().getGlobalErrors();
List<String> errors = new ArrayList<>(fieldErrors.size() + globalErrors.size());
String error;
for (FieldError fieldError : fieldErrors) {
error = fieldError.getField() + ", " + fieldError.getDefaultMessage();
errors.add(error);
}
for (ObjectError objectError : globalErrors) {
error = objectError.getObjectName() + ", " + objectError.getDefaultMessage();
errors.add(error);
}
ErrorMessage errorMessage = new ErrorMessage(errors);
Object result=errorMessage;
//Object result=ex.getBindingResult();//instead of above can allso pass the more detailed bindingResult
return new ResponseEntity(result, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ResponseEntity<ErrorMessage> handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex
) {
String unsupported = "Unsupported content type: " + ex.getContentType();
String supported = "Supported content types: " + MediaType.toString(ex.getSupportedMediaTypes());
ErrorMessage errorMessage = new ErrorMessage(unsupported, supported);
return new ResponseEntity(errorMessage, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
}
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ErrorMessage> handleHttpMessageNotReadable(HttpMessageNotReadableException ex) {
Throwable mostSpecificCause = ex.getMostSpecificCause();
ErrorMessage errorMessage;
if (mostSpecificCause != null) {
String exceptionName = mostSpecificCause.getClass().getName();
String message = mostSpecificCause.getMessage();
errorMessage = new ErrorMessage(exceptionName, message);
} else {
errorMessage = new ErrorMessage(ex.getMessage());
}
return new ResponseEntity(errorMessage, HttpStatus.BAD_REQUEST);
}
}
A few more related classes referred by the ControllerAdvice
follow.
package sample;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class ErrorMessage {
private List<String> errors;
public ErrorMessage() {
}
public ErrorMessage(List<String> errors) {
this.errors = errors;
}
public ErrorMessage(String error) {
this(Collections.singletonList(error));
}
public ErrorMessage(String ... errors) {
this(Arrays.asList(errors));
}
public List<String> getErrors() {
return errors;
}
public void setErrors(List<String> errors) {
this.errors = errors;
}
}
package sample;
public class Problem {
private String logRef;
private String message;
public Problem(String logRef, String message) {
super();
this.logRef = logRef;
this.message = message;
}
public Problem() {
super();
}
public String getLogRef() {
return logRef;
}
public void setLogRef(String logRef) {
this.logRef = logRef;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Let's restart the application and try again repeatedly until we get the problem response.
Yes, it works. But there is an issue. What is this "undocumented" text? Let's look at what our Swagger UI is showing as the default response codes for our controller.
Where is the documentation in above regarding the "Problem" model we are using when reporting exceptions? Also, what about customizing the above-listed codes and details? Yes, it's possible in different ways.
Let's edit our SwaggerConfig.java and add some new code:
private String resolveName(Class clazz) {
return clazz.getSimpleName();
}
private Docket changeGlobalResponses(Docket docket) {
RequestMethod[] methodsToCustomize = {
RequestMethod.GET, RequestMethod.POST, };
docket = docket.useDefaultResponseMessages(false);
for (RequestMethod methodToCustomize : methodsToCustomize) {
docket = docket
.globalResponseMessage(methodToCustomize,
Lists.newArrayList(
new ResponseMessageBuilder().code(
HttpStatus.INTERNAL_SERVER_ERROR.value())
.message("Server error Try again later")
.responseModel(new ModelRef(resolveName(Problem.class)))
.build(),
new ResponseMessageBuilder().code(
HttpStatus.BAD_REQUEST.value())
.message("Bad Request")
.responseModel(new ModelRef(resolveName(ErrorMessage.class)))
.build()));
}
return docket;
}
Also, please add the following imports:
import com.google.common.collect.Lists;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.builders.ResponseMessageBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
Below, we show the completed SwaggerConfig.java with an additional invocation of the changeGlobalResponses()
method:
package sample;
import java.util.Collections;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
ApiInfo apiInfo() {
return new ApiInfo("A sample spring swagger example",
"This project demonstrates some spring "
+ "and swagger concepts. It also demonstrates "
+ "some enhancements in spring swagger using "
+ "spring-swagger-simplified",
"1.0.4",
"Use as you like- some url here",
new Contact("Contact Name",
"https://bitbucket.org/tek-nik/simplified-swagger-examples/", null),
"The Apache Software License, Version 2.0",
"http://www.apache.org/licenses/LICENSE-2.0.txt",
Collections.EMPTY_LIST);
}
@Bean
public Docket api() {
return changeGlobalResponses(new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(Predicates.not(RequestHandlerSelectors.
basePackage("org.springframework.boot")))
.paths(PathSelectors.any())
.build()).apiInfo(apiInfo())
;
}
private String resolveName(Class clazz) {
return clazz.getSimpleName();
}
private Docket changeGlobalResponses(Docket docket) {
RequestMethod[] methodsToCustomize = {
RequestMethod.GET, RequestMethod.POST, };
docket = docket.useDefaultResponseMessages(false);
for (RequestMethod methodToCustomize : methodsToCustomize) {
docket = docket
.globalResponseMessage(methodToCustomize,
Lists.newArrayList(
new ResponseMessageBuilder().code(
HttpStatus.INTERNAL_SERVER_ERROR.value())
.message("Server error Try again later")
.responseModel(new ModelRef(resolveName(Problem.class)))
.build(),
new ResponseMessageBuilder().code(
HttpStatus.BAD_REQUEST.value())
.message("Bad Request")
.responseModel(new ModelRef(resolveName(ErrorMessage.class)))
.build()));
}
return docket;
}
}
-
As can be seen above, when building the docket, we are changing the global responses in line 45. Make sure you are doing the same, or else copy from the above SwaggerConfig.java
Slightly better. All it proves so far is that we can customize the response codes by one centralized means. (There are multiple means). But where is the Problem model documented?
You might have also noticed this annoying message that has been flashing whenever we enter the controller.
It's basically complaining that under model definitions, there is no model named Problem
or ErrorMessage
.
There is only a Person
model. Again, there are multiple means of introducing the "Problem
" model into our model definitions.
Let's do a few changes.
Firstly, let's edit our pom.xml and uncomment our artifact we had previously commented.
<dependency>
<groupId>org.bitbucket.tek-nik</groupId>
<version>1.0.4</version>
<artifactId>spring-swagger-simplified</artifactId>
</dependency>
Secondly, let's edit our SwaggerConfig
slightly using the method "resolveName
."
From:
private String resolveName(Class clazz) {
return clazz.getSimpleName();
}
Let's change it to:
private String resolveName(Class clazz) {
return clazz.getName();
}
That's because <artifactId>spring-swagger-simplified</artifactId> also forces Swagger to use fully qualified names for the classes.
Let's build and restart. This is what we see now:
Now, let's look at our response codes for the controller.
How did this happen? The spring-swagger-simplified artifact also detects @ControllerAdvice
decorated classes and can derive the needed models automatically.
Our automatic min, max, etc. is also back in the model definitions, as shown below. (You can verify by clicking on the "Model" link in above screen — next to "Example Value")
Of course, you can inspect the model definitions in various other places also in the Swagger UI with the same automatic detail of min, max, and other constraints, which cause tighter contracts between APIs.
Note: We just leveraged standard javax.validation.constraints annotations. We did not clutter our code with swagger annotations to convey these constraints information in swagger documentation.
That concludes this tutorial.
Again, this was only a brief introduction to the capabilities of this jar along with being a tutorial on Swagger apiInfo
and Spring Global Exception handling integration with Swagger. For a more complete understanding of the various features, please try out this more detailed example project with many more features — https://bitbucket.org/tek-nik/simplified-swagger-examples/.
We will next discuss the usage of Swagger in OAuth space along with Spring and spring-swagger-simplified. With this, we will start covering the various other spring and swagger integration topics one by one and the role of spring-swagger-simplified jar.
Stay tuned!.
Meanwhile, the complete code for this article can be found here, i.e. in branch "doingmore."
Opinions expressed by DZone contributors are their own.
Comments