39

Doing More With Swagger and Spring

 4 years ago
source link: https://www.tuicool.com/articles/6f2IVjn
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

In mylast 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 previousarticle.

6JBbMvM.png!web

IjeyUf7.png!web

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 .

j6nmyyr.png!web

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."

MVBzaqr.png!web

YFJraqu.png!web

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.

FzAZ7re.png!web

Let's get back to invoking the controller and seeing what happens when it fails by invoking it repeatedly.

UJ3ANjY.png!web

Let's fill up valid data (keep it handy say in notepad to copy from when needed):

{
"age": 18,
"creditCardNumber": "4111111111111111",
"email": "[email protected]",
"email1": "[email protected]",
"firstName": "string",
"lastName": "string"
}

Now, press the execute button until we get an error response, as shown here:

BruI73A.png!web

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.

u2QRnaV.png!web

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.

rQZVjaJ.png!web

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

mEzeyeq.png!web

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?

biYV32f.png!web

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 .

U3uai27.png!web

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:

B7NraeE.png!web

Now, let's look at our response codes for the controller.

imMNruB.png!web

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")

YFbaqmY.png!web

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." 


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK