Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Developing Spring Cloud Microservices With a Central Environment

DZone's Guide to

Developing Spring Cloud Microservices With a Central Environment

Want to make sure your microservices don't get confused by all that communicating they have to do? Learn how it's possible with Spring Cloud.

· Microservices Zone ·
Free Resource

Record growth in microservices is disrupting the operational landscape. Read the Global Microservices Trends report to learn more.

How can developers work simultaneously on the same central Spring Cloud environment?

Developing software based on a microservices architecture is extremely easy when you use Spring Boot and Spring Cloud. Just throw in a few lines of code and you can have a microservice up and running. But how do you develop a real-world application in such an environment? In theory, each microservice is isolated and can be developed alone, but, in practice, this is often not the case. To develop and test your service in the context of the application that uses it requires more than just your microservice to be up and running. So, how can one conveniently develop in a multi-microservices environment?

Well, if you only need two or three services, then you can run them all locally, so setting up such an environment is not a big deal. But, if your application is composed of dozens of services (a common case for large apps) then starting such an environment, keeping it up to date, etc., can turn into a real headache. Going to the other extreme and running only one microservice locally and the rest on some central server(s) is also a problem. If, for example, I’m developing the ‘MyService’ microservice, and the central environment also has a ‘MyService’ service running (all developers are using this environment so it has all services running there), which ‘MyService’ will be used (as the common discovery service holds reference to both)? When numerous developers are working with such an environment the problem gets worse, of course.

We have found an elegant way to enjoy both worlds — each developer is locally only running the service or two that they are currently working on, while all other services run on some central environment. This way. we manage to avoid collisions and confusions between the instances of that service!

How does this magic happen? Well, the root of our initial problem is the fact that the service a developer is working on and its matching instance on the central environment (and on other developer's machine) are registered by the same name on the discovery service (we use Eureka, by the way). What if each instance would register itself by a different name and still be usable by any other service that needs it? Well, it is possible, but it's a bit tricky. These are the needed steps:

1. For each service (or in one infrastructure jar all the services are using) we define a @Configuration bean (RemoteEurekaConfig) to tweak the registration to Eureka. In this class, we return a EurekaInstanceConfigBean that overrides the superclass behavior by adding the hostname to the registered service. This way 'MyService' will be registered as 'MyHostName.MyService' so I and my fellow developers each have a unique name for this service, allowing us to work on it simultaneously (and not colliding with the ‘MyService’ instance on the central environment, so other can use it when they develop other services). We determine if we want to have this behavior or not by setting an application property to true (named devDiscovery in my example). You can also use the current profile (dev/prod) or any other desired flag to decide whether your service should change the default discovery service registration or not. In addition, you can set  newAppName to whatever unique value you desire (developer name, the version you work on, etc.) as long as it is unique among developers and meaningful enough for you.

Listing 1 - RemoteEurekaConfig

@Value("${dev.discovery:false}")

 private Boolean devDiscovery;

@Bean
@Autowired
@Profile("development")
public EurekaInstanceConfigBean eurekaInstanceConfigBean(final InetUtils inetUtils) {
                                String newAppName = getHostname() + "." + appName;
                                config = new EurekaInstanceConfigBean(inetUtils) {
                                                @Override
                                                public void setEnvironment(Environment environment) {
                                                                super.setEnvironment(environment);
                                                                if (devDiscovery != null && devDiscovery == true) {
                                                                                setAppname(newAppName);
                                                                                setVirtualHostName(newAppName);
                                                                                setSecureVirtualHostName(newAppName);
                                                                }
                                                }
                                };
                                config.setNonSecurePort(port);
                                config.setIpAddress(getHostAddress());
                                config.getMetadataMap().put("instanceId", config.getHostname() + ":" +                                                                                                            config.getAppname() + ":" + port);
                                return config;
  }

2. Now, the fact that our service is registered on the remote Eureka with a unique name is not enough, since our gateway will still route UI (or other) requests to instances of 'MyService' and not 'MyHostName.MyService' since these are the routings defined for the gateway. Yes, we can modify these routes in the application.properties file whenever we run a service locally, but that can be error-prone and tedious. We can do better. We can handle this dynamically by defining a new bean, DynamicRouting, which at initialization time will go over all registered services, and update the local routes to the services that run locally. How does it know which services run locally? Easy. This kind of service will have our unique prefix, of course! We should iterate all registered services since we might have more than one service running locally. Of course, that with this solution locally running services should be started before the gateway. If a more dynamic behavior is required, we can apply this logic every X seconds to always be up to date (although I find it to be overkill in most cases).

Listing 2 - DynamicRouting

@Autowired
            private ZuulProperties zuulProperties;       
            @Autowired
            DiscoveryClient discoveryClient
            @PostConstruct
            public void init() {
                        // Get all services from Eureka
                        List<String> allServices = discoveryClient.getServices();
                        String prefix =  getHostname()+".";
                        for(String service : allServices) {
                        // If a service starts with my designated prefix, replace the original route to it
                               if (service.startsWith(prefix)) {
                                       String originalService = service.substring(service.indexOf(".")+1);
                                   for(ZuulProperties.ZuulRoute route : zuulProperties.getRoutes().values())   
                                      {      
                                              if (route.getServiceId().equals(originalService))  {
                                                // Change original route to 'my' service id
                                                            route.setServiceId(service);
                                                }
                                     }
                              }
                     }
            }            

3. We are almost done! One last thing. In case you call services using REST directly from other services and not through the gateway, you'll have to take care of that too. If you use Spring's RestTemplate, for example, you'll have to wrap it and apply the same logic as above, meaning:

  • Decide if this is a service call or a call to an actual URL (and do nothing for the latter). For example, a service call will look like http://MyService/sth/1.

  • Check if the service call is a call to a locally running service by querying Eureka as we did above and check the prefixes.

  • If so, alter the host part of the URL to the locally running service name as we did before. For example, http://myHostName.MyService/sth/1 and the local service shall be called!

That's it! You can now have a system with hundreds of microservices happily running on one central environment, and developers can develop while running only one service locally, saving resources and time while always being in sync automatically.

Two things to remember:

  • The relevant beans shown here should be annotation with @Profile("development") and should not be active outside development, to avoid confusion.

  • Since we write full stack in my workplace we always have the gateway running locally. If you only need the backend you do not have to run the gateway locally, just use Swagger or Postman (or similar) to call the service APIs. In case you do want to use the application's UI and don't want to run the gateway locally, this can also be done by adding some information to the application's URL (the local services prefix, and the generic names of the locally running services). Then the UI can easily replace REST calls addresses to the local service names (as we did in the server) and they will be routed to the developer's machine.

Learn why microservices are breaking traditional APM tools that were built for monoliths.

Topics:
spring cloud ,microservices ,microservices architecture java ,java tutorial ,spring cloud tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}