18 How to Unify Management of Apis Across Multiple Modules and Aggregate Apis

18 How to Unify Management of APIs Across Multiple Modules and Aggregate APIs #

In the chapter “First Spring Boot Subservice - Member Service”, we have already implemented the integration of Swagger2, which provides the interface display and testing functions through UI. When there are multiple microservices or when external clients access the API, they will face numerous Swagger interfaces - various server ports and interface paths, which increases the difficulty of calling. At this time, it is urgent to integrate and present all APIs in one page for external access.

Two Implementation Approaches #

Since Swagger has been integrated into each sub-module, we can access each API through a specified path. There are two ways to manage all APIs.

  1. Create a single page on your own, and include all sub-module swagger-ui pages in it. The interfaces can be accessed in the page through iframes. This approach is simple and crude, but it has the following drawbacks:
  • The sub-module APIs are exposed externally, which can easily lead to security risks. The functions that need to be handled uniformly by the gateway, such as authentication, are missing in the sub-modules.
  • Without going through the gateway, accessing the ports of each sub-module is not uniform, which increases the chance of coding errors.
  • If there are changes in the modules, the single page needs to be kept up to date.
  1. Integrate the swagger-ui of each module at the gateway layer, and the three aforementioned drawbacks are not a problem.

In this example, we will integrate all swagger-ui pages through the Gateway service layer and present them externally in a unified manner.

Integrating Swagger at the Gateway Layer #

First, let’s take a look at what is loaded when accessing swagger-ui.html. Taking the point service as an example, if using the Chrome browser, right-click and open the “Inspect” feature, switch to the Network tab, and sort by the “Type” column. After refreshing the page, the default call involves 4 asynchronous methods:

http://localhost:10061/swagger-resources/configuration/ui
http://localhost:10061/swagger-resources/configuration/security
http://localhost:10061/swagger-resources
http://localhost:10061/v2/api-docs

img

There is also a key springfox.js file shown in the above figure. The data displayed in the interface is ultimately obtained from the /v2/api-docs method, which is requested after calling the swagger-resources method. The response of the swagger-resources method is:

[{"name":"default","url":"/v2/api-docs","swaggerVersion":"2.0","location":"/v2/api-docs"}]

During the UI initialization process, the URL address /v2/api-docs is loaded to obtain all the interface configuration data. As shown below, after formatting the data, you can clearly see the JSON data defining the interfaces:

img

img

So, the key point is to integrate the /v2/api-docs of each sub-module into the same UI interface, which can achieve the goal of aggregating all APIs. Let’s get started.

The way to introduce Swagger in the previous sub-service is the same. Add the configuration items to the pom.xml file:

<!--swagger2 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
</dependency>

Swagger Configuration #

From the implementation principle of Swagger2, the key to API interface resources lies in the core interface class SwaggerResourcesProvider, and the only implementation class is InMemorySwaggerResourcesProvider. When Swagger2 works in each sub-module, it is processed by this class and then returned to the front end.

// Core method
@Override
  public List<SwaggerResource> get() {
    List<SwaggerResource> resources = new ArrayList<SwaggerResource>();

    for (Map.Entry<String, Documentation> entry : documentationCache.all().entrySet()) {
      String swaggerGroup = entry.getKey();
      if (swagger1Available) {
        SwaggerResource swaggerResource = resource(swaggerGroup, swagger1Url);
        swaggerResource.setSwaggerVersion("1.2");
        resources.add(swaggerResource);
      }

      if (swagger2Available) {
        SwaggerResource swaggerResource = resource(swaggerGroup, swagger2Url);
        swaggerResource.setSwaggerVersion("2.0");
        resources.add(swaggerResource);
      }
    }
    Collections.sort(resources);
    return resources;
  }

  private SwaggerResource resource(String swaggerGroup, String baseUrl) {
    SwaggerResource swaggerResource = new SwaggerResource();
    swaggerResource.setName(swaggerGroup);
    swaggerResource.setUrl(swaggerLocation(baseUrl, swaggerGroup));
    return swaggerResource;
  }

When integrating at the gateway, this method needs to be overridden to add all the sub-module SwaggerResources of the routes, forming a data collection, and then selecting the corresponding service module on the UI to call different /v2/api-docs methods and display them.

@Component
public class ParkingSwaggerResourcesProvider implements SwaggerResourcesProvider {

    /**
     * Swagger2 specific resource address
     */
    private static final String SWAGGER2URL = "/v2/api-docs";

    /**
     * Gateway route locator
     */
    private final RouteLocator routeLocator;

    /**
     * ...
/**
 * Application name, needs to be excluded in the following text
 */
@Value("${spring.application.name}")
private String curApplicationName;

public ParkingSwaggerResourcesProvider(RouteLocator routeLocator) {
    this.routeLocator = routeLocator;
}

@Override
public List<SwaggerResource> get() {
    List<SwaggerResource> resources = new ArrayList<>();
    List<String> routeHosts = new ArrayList<>();
    // Get the serviceId of all applications from the gateway configuration
    routeLocator.getRoutes().filter(route -> route.getUri().getHost() != null)
            .filter(route -> !curApplicationName.equals(route.getUri().getHost()))
            .subscribe(route -> routeHosts.add(route.getUri().getHost()));

    Set<String> allUrls = new HashSet<>();
    routeHosts.forEach(instance -> {
        // /serviceId/v2/api-info, when the gateway calls this interface, it will automatically find the corresponding service instance
        String url = "/" + instance + SWAGGER2URL;
        if (!allUrls.contains(url)) {
            allUrls.add(url);
            SwaggerResource swaggerResource = new SwaggerResource();
            swaggerResource.setUrl(url);
            //swaggerResource.setLocation(url); location is deprecated, use url instead
            swaggerResource.setName(instance);
            resources.add(swaggerResource);
        }
    });
    return resources;
}

Override the SwaggerResourceController class and replace the default loading methods of springfox.js as follows:

@RestController
@RequestMapping("/swagger-resources")
public class SwaggerResourceController {

    private ParkingSwaggerResourcesProvider swaggerResourceProvider;

    @Autowired
    public SwaggerResourceController(ParkingSwaggerResourcesProvider swaggerResourceProvider) {
        this.swaggerResourceProvider = swaggerResourceProvider;
    }

    @RequestMapping(value = "/configuration/security")
    public ResponseEntity<SecurityConfiguration> securityConfiguration() {
        return new ResponseEntity<>(SecurityConfigurationBuilder.builder().build(), HttpStatus.OK);
    }

    @RequestMapping(value = "/configuration/ui")
    public ResponseEntity<UiConfiguration> uiConfiguration() {
        return new ResponseEntity<>(UiConfigurationBuilder.builder().build(), HttpStatus.OK);
    }

    @RequestMapping
    public ResponseEntity<List<SwaggerResource>> swaggerResources() {
        return new ResponseEntity<>(swaggerResourceProvider.get(), HttpStatus.OK);
    }
}

Start the gateway layer and access http://localhost:10091/swagger-ui.html. It is the same as accessing a single child module’s swagger-ui. You will see the following page, indicating that the basic configuration is successful. The dropdown box in the upper right corner shows the names of the child services, but the interface data is not displayed. The exception message is “Please log in first.”

img

We can think of the JWTFilter filter that filters all requests for token verification. The exception here is thrown from the verification. We need to exclude some unnecessary paths from authentication and add the swagger-ui-related requests to the whitelist to skip token verification and display the interface normally.

Modify the application.yml configuration to exclude the relevant backend services from authentication:

jwt:
  skip-urls:
  - /member-service/member/bindMobile
  - /member-service/member/logout
  - /member-service/test/hello
  - /card-service/v2/api-docs
  - /resource-service/v2/api-docs
  - /member-service/v2/api-docs    
  - /charging-service/v2/api-docs
  - /finance-service/v2/api-docs

Restart the gateway project to verify whether the swagger-ui.html of the gateway layer is correct. The following screenshot shows that the interface requests of each service module can be obtained.

img

Testing Interface #

We locate the test interface hello in the member service module and check if the hello method can be called properly.

img

As you can see, the request path has been changed to the address and port of the gateway. The port of the backend child module has been hidden, and the swagger-ui provided by the gateway layer is used for external application calls.

It can be seen that the Gateway solution is similar to the first simple method. Both list the resource addresses and select to reload the data into the specified area. However, the second Gateway method does not require additional maintenance of the page, and Swagger itself is responsible for updating.

This article aggregates all the APIs of the microservices, greatly reducing the complexity of frontend calls and also making progress in development experience.

Consider the following question:

  • How can we meet the requirements of opening different API collections for different ports?