library Archives - Piotr's TechBlog https://piotrminkowski.com/tag/library/ Java, Spring, Kotlin, microservices, Kubernetes, containers Tue, 31 Aug 2021 15:21:41 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.1 https://i0.wp.com/piotrminkowski.com/wp-content/uploads/2020/08/cropped-me-2-tr-x-1.png?fit=32%2C32&ssl=1 library Archives - Piotr's TechBlog https://piotrminkowski.com/tag/library/ 32 32 181738725 Guide to building Spring Boot library https://piotrminkowski.com/2020/08/04/guide-to-building-spring-boot-library/ https://piotrminkowski.com/2020/08/04/guide-to-building-spring-boot-library/#comments Tue, 04 Aug 2020 08:32:58 +0000 http://piotrminkowski.com/?p=8270 In this article, I’m going to show you how to create and share your own custom Spring Boot library. If you decide to build such a product you should follow some best practices recommended by Spring Team. It’s a little bit more complicated than creating a plain Java library. Finally, you should publish your artifacts […]

The post Guide to building Spring Boot library appeared first on Piotr's TechBlog.

]]>
In this article, I’m going to show you how to create and share your own custom Spring Boot library. If you decide to build such a product you should follow some best practices recommended by Spring Team. It’s a little bit more complicated than creating a plain Java library. Finally, you should publish your artifacts somewhere to share it with the community. Probably you need to obtain positive feedback from the community, so you should think about adding some extras. I’m also going to describe them. Let’s begin!

Examples

If you are looking for the examples of simple Spring Boot libraries you can take a look on my repositories: https://github.com/piomin/spring-boot-logging and https://github.com/piomin/spring-boot-istio.

1. Pick the right name

We should pick the right name for our library. Spring recommends creating special modules called “starters” that contain code with auto-configuration and customize the infrastructure of a given technology. The name of the third-party starter should end with spring-boot-starter and start with the name of the project or something related to the technology we are using in the library. It is contrary to the names of all official starters, which are created following the pattern spring-boot-starter-*. For example, the names of my libraries are logstash-logging-spring-boot-starter or istio-spring-boot-starter.

2. Create auto-configuration

Typically the “starter” module is separated from the “autoconfigure” module. However, it is not required. The autoconfigure module contains everything necessary for a start. Moreover, if I’m creating a simple library that does not consist of many classes, I’m inserting everything into a single starter module. Of course, that is my approach. You can still create a separate starter module that includes the required dependencies for the project. It is the most important that all the beans are registered inside the auto-configuration class. Do not annotate each of your beans inside a library with @Component or @Service, but define them in an auto-configured module. Here’s a simple auto-configuration class inside my logstash-logging-spring-boot-starter library.

 

@Configuration
@ConfigurationProperties(prefix = "logging.logstash")
public class SpringLoggingAutoConfiguration {

   private static final String LOGSTASH_APPENDER_NAME = "LOGSTASH";
   private String url = "localhost:8500";
   private String ignorePatterns;
   private boolean logHeaders;
   private String trustStoreLocation;
   private String trustStorePassword;

   @Value("${spring.application.name:-}")
   String name;

   @Autowired(required = false)
   Optional<RestTemplate> template;

   @Bean
   public UniqueIDGenerator generator() {
      return new UniqueIDGenerator();
   }

   @Bean
   public SpringLoggingFilter loggingFilter() {
      return new SpringLoggingFilter(generator(), ignorePatterns, logHeaders);
   }

   @Bean
   @ConditionalOnMissingBean(RestTemplate.class)
   public RestTemplate restTemplate() {
      RestTemplate restTemplate = new RestTemplate();
      List<ClientHttpRequestInterceptor> interceptorList = new ArrayList<ClientHttpRequestInterceptor>();
      interceptorList.add(new RestTemplateSetHeaderInterceptor());
      restTemplate.setInterceptors(interceptorList);
      return restTemplate;
   }

   @Bean
   @ConditionalOnProperty("logging.logstash.enabled")
   public LogstashTcpSocketAppender logstashAppender() {
      LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
      LogstashTcpSocketAppender logstashTcpSocketAppender = new LogstashTcpSocketAppender();
      logstashTcpSocketAppender.setName(LOGSTASH_APPENDER_NAME);
      logstashTcpSocketAppender.setContext(loggerContext);
      logstashTcpSocketAppender.addDestination(url);
      if (trustStoreLocation != null) {
         SSLConfiguration sslConfiguration = new SSLConfiguration();
         KeyStoreFactoryBean factory = new KeyStoreFactoryBean();
         factory.setLocation(trustStoreLocation);
         if (trustStorePassword != null)
            factory.setPassword(trustStorePassword);
         sslConfiguration.setTrustStore(factory);
         logstashTcpSocketAppender.setSsl(sslConfiguration);
      }
      LogstashEncoder encoder = new LogstashEncoder();
      encoder.setContext(loggerContext);
      encoder.setIncludeContext(true);
      encoder.setCustomFields("{\"appname\":\"" + name + "\"}");
      encoder.start();
      logstashTcpSocketAppender.setEncoder(encoder);
      logstashTcpSocketAppender.start();
      loggerContext.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(logstashTcpSocketAppender);
      return logstashTcpSocketAppender;
   }
}

To enable auto-configuration for the custom library we need to create file spring.factories in /src/main/resources/META-INF directory that contains a list of auto-configuration classes.

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
pl.piomin.logging.config.SpringLoggingAutoConfiguration

 

3. Process annotations

Spring is an annotation-based framework. If you are creating your custom library you will usually define some annotations used to enable or disable features. With Spring Boot you can easily process such annotations. Here’s my custom annotation used to enable the Istio client on application startup. I’m following the Spring pattern widely used in Spring Cloud.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableIstio {
    int timeout() default 0;
    String version() default "";
    int weight() default 0;
    int numberOfRetries() default 0;
    int circuitBreakerErrors() default 0;
}

I need to process already defined annotation only once on startup. That’s why I’m creating a bean that implements the ApplicationListener interface to catch ContextRefreshedEvent emitted by Spring Boot.

public class ApplicationStartupListener implements
      ApplicationListener<ContextRefreshedEvent> {
   private ApplicationContext context;
   private EnableIstioAnnotationProcessor processor;
   public ApplicationStartupListener(ApplicationContext context,
         EnableIstioAnnotationProcessor processor) {
      this.context = context;
      this.processor = processor;
   }
   @Override
   public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
      Optional<EnableIstio> annotation =
            context.getBeansWithAnnotation(EnableIstio.class).keySet().stream()
            .map(key -> context.findAnnotationOnBean(key, EnableIstio.class))
            .findFirst();
      annotation.ifPresent(enableIstio -> processor.process(enableIstio));
   }
}

 

4. Spring Boot library dependencies

Our library should reference only those artifacts or other starters, that are necessary for implementation. Here’s a minimal set of artifacts required for my istio-spring-boot-starter. Besides Spring and Spring Boot libraries I only use Kubernetes and Istio Java clients. We might as well declare a reference to spring-boot-starter-parent.

<dependencies>
   <dependency>
      <groupId>me.snowdrop</groupId>
      <artifactId>istio-client</artifactId>
      <version>${istio-client.version}</version>
   </dependency>
   <dependency>
      <groupId>io.fabric8</groupId>
      <artifactId>kubernetes-client</artifactId>
      <version>${kubernetes-client.version}</version>
   </dependency>
   <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context-support</artifactId>
      <version>${spring.version}</version>
      <scope>provided</scope>
   </dependency>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-autoconfigure</artifactId>
      <version>${spring.boot.version}</version>
      <scope>provided</scope>
   </dependency>
</dependencies>

 

5. Publish

Typically the implementation process of the Spring Boot library is divided into two phases. In the first phase, we need to implement the specific mechanisms related to our library. In the second phase, we should take care of following the Spring Boot best practices. Assuming we have already finished it, we may publish our custom starter to share it with the community. In my opinion, the best way to do it is by publishing it on the Maven Central repository.
You must go through several steps to publish JAR files to Maven Central. The list of necessary steps is listed below. For the more detailed description, you may refer to the article How to Publish Your Artifacts to Maven Central on DZone.
Here’s the list of prerequisites:

  • Create an account at Sonatype (https://oss.sonatype.org/)
  • Claim your product’s namespace by creating an issue in Sonatype’s Jira
  • Generate PGP private/public key pair to sign your JAR files
  • Publish your key to a public key server to one of GPG servers

After completing all the required steps you may proceed to a configuration in Maven POM file. You need to include there two sets of configurations. The first of them contains a piece of the necessary information about our project, author, and source code repository.

<name>logstash-logging-spring-boot-starter</name>
<description>Library for HTTP logging with Spring Boot</description>
<url>https://github.com/piomin/spring-boot-logging</url>
<developers>
   <developer>
      <name>Piotr Mińkowski</name>
      <email>piotr.minkowski@gmail.com</email>
      <url>https://github.com/piomin</url>
   </developer>
</developers>
<licenses>
   <license>
      <name>MIT License</name>
      <url>http://www.opensource.org/licenses/mit-license.php</url>
      <distribution>repo</distribution>
   </license>
</licenses>
<scm>
   <connection>scm:git:git://github.com/piomin/spring-boot-logging.git</connection>
   <developerConnection>scm:git:git@github.com:piomin/spring-boot-logging.git</developerConnection>
   <url>https://github.com/piomin/spring-boot-logging</url>
</scm>
<distributionManagement>
   <snapshotRepository>
      <id>ossrh</id>
      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
   </snapshotRepository>
   <repository>
      <id>ossrh</id>
      <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
   </repository>
</distributionManagement>

In the second step, we need to add some Maven plugins for signing JAR file, including source code files and Javadocs there. Here’s a required list of plugins activated only with release Maven profile enabled.

profiles>
   <profile>
      <id>release</id>
      <build>
         <plugins>
            <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-gpg-plugin</artifactId>
               <version>1.6</version>
               <executions>
                  <execution>
                     <id>sign-artifacts</id>
                     <phase>verify</phase>
                     <goals>
                        <goal>sign</goal>
                     </goals>
                  </execution>
               </executions>
            </plugin>
            <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-source-plugin</artifactId>
               <version>3.2.1</version>
               <executions>
                  <execution>
                     <id>attach-sources</id>
                     <goals>
                        <goal>jar-no-fork</goal>
                     </goals>
                  </execution>
               </executions>
            </plugin>
            <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-javadoc-plugin</artifactId>
               <version>3.2.0</version>
               <executions>
                  <execution>
                     <id>attach-javadocs</id>
                     <goals>
                        <goal>jar</goal>
                     </goals>
                  </execution>
               </executions>
            </plugin>
         </plugins>
      </build>
   </profile>
</profiles>

Finally you need to execute command mvn clean deploy -P release, and visit Sonatype site to confirm publication of your library.

spring-boot-library-sonatype

6. Promote your Spring Boot library

Congratulations! You have already published your first Spring Boot library. But, the question is what’s next? You probably would like to encourage people to try it. Of course, you can advertise it on social media, or create articles on dev portals. But my first advice is to take care of the presentation site. If you are storing your source code on GitHub prepare a Readme file with a detailed description of your library. It is also worth adding some tags that describe your project.

spring-boot-library-github-2

It is relatively easy to integrate your GitHub repository with some third-party tools used for continuous integration or static source code analysis. Thanks to that you can continuously improve your library. Moreover, you can add some badges to your repository that indicate you are using such tools. In my repositories spring-boot-logging and spring-boot-istio, I have already added badges with Maven release, CircleCI builds status and SonarCloud analysis reports. Looks fine? 🙂

spring-boot-library-github-1

Conclusion

In this article, I describe the process of creating a Spring Boot library from the beginning to the end. You can take a look on my libraries https://github.com/piomin/spring-boot-logging and https://github.com/piomin/spring-boot-istio if you are looking for simple examples. Of course, there are many other third-party Spring Boot starters published on GitHub you can also take a look. If you interested in building your own Spring Boot library you should learn more about auto-configuration: A Magic Around Spring Boot Auto Configuration.

The post Guide to building Spring Boot library appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2020/08/04/guide-to-building-spring-boot-library/feed/ 3 8270
Spring Boot Library for integration with Istio https://piotrminkowski.com/2020/06/10/spring-boot-library-for-integration-with-istio/ https://piotrminkowski.com/2020/06/10/spring-boot-library-for-integration-with-istio/#comments Wed, 10 Jun 2020 15:17:10 +0000 http://piotrminkowski.com/?p=8102 In this article I’m going to present an annotation-based Spring Boot library for integration with Istio. The Spring Boot Istio library provides auto-configuration, so you don’t have to do anything more than including it to your dependencies to be able to use it. The library is using Istio Java Client me.snowdrop:istio-client for communication with Istio […]

The post Spring Boot Library for integration with Istio appeared first on Piotr's TechBlog.

]]>
In this article I’m going to present an annotation-based Spring Boot library for integration with Istio. The Spring Boot Istio library provides auto-configuration, so you don’t have to do anything more than including it to your dependencies to be able to use it.
The library is using Istio Java Client me.snowdrop:istio-client for communication with Istio API on Kubernetes. The following picture illustrates an architecture of the presented solution on Kubernetes. The Spring Boot Istio is working just during application startup. It is able to modify existing Istio resources or create the new one if there are no matching rules found.
spring-boot-istio-arch

Source code

The source code of library is available on my GitHub repository https://github.com/piomin/spring-boot-istio.git.

How to use it

To use in your Spring Boot application you include the following dependency.

<dependency>
   <groupId>com.github.piomin</groupId>
   <artifactId>spring-boot-istio</artifactId>
   <version>0.1.0.RELEASE</version>
</dependency>

After that you should annotate one of your class with @EnableIstio. The annotation contains several fields used for Istio DestinationRule and VirtualService objects.

@RestController
@RequestMapping("/caller")
@EnableIstio(version = "v1", timeout = 3, numberOfRetries = 3)
public class CallerController {

    private static final Logger LOGGER = LoggerFactory.getLogger(CallerController.class);

    @Autowired
    BuildProperties buildProperties;
    @Autowired
    RestTemplate restTemplate;
    @Value("${VERSION}")
    private String version;

    @GetMapping("/ping")
    public String ping() {
        LOGGER.info("Ping: name={}, version={}", buildProperties.getName(), version);
        String response = restTemplate.getForObject("http://callme-service:8080/callme/ping", String.class);
        LOGGER.info("Calling: response={}", response);
        return "I'm caller-service " + version + ". Calling... " + response;
    }
   
}

The name of Istio objects is generated based on spring.application.name. So you need to provide that name in your application.yml.

spring:
  application:
    name: caller-service

Currently there are five available fields that may be used for @EnableIstio.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableIstio {
    int timeout() default 0;
    String version() default "";
    int weight() default 0;
    int numberOfRetries() default 0;
    int circuitBreakerErrors() default 0;
}

Here’s the detailed description of available parameters.

  • version – it indicates the version of IstioSubset. We may define multiple versions of the same application. The name of label is version
  • weight – it sets a weight assigned to the Subset indicated by the version label
  • timeout – a total read timeout in seconds on the client side – including retries
  • numberOfRetries – it enables retry mechanism. By default we are retrying all 5XX HTTP codes. The timeout of a single retry is calculated as timeout / numberOfRetries
  • circuitBreakerErrors – it enables circuit breaker mechanism. It is based on a number of consecutive HTTP 5XX errors. If circuit is open for a single application it is ejected from the pool for 30 seconds

How it works

Here’s the Deployment definition of our sample service. It should be labelled with the same version as set inside @EnableIstio.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: caller-service
spec:
  replicas: 1
  selector:
    matchLabels:
      app: caller-service
  template:
    metadata:
      name: caller-service
      labels:
        app: caller-service
        version: v1
    spec:
      containers:
      - name: caller-service
        image: piomin/caller-service
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080

Let’s deploy our sample application on Kubernetes. After deploy we may verify the status of Deployment.

spring-boot-istio-deployment

The name of created DestinationRule is a concatenation of spring.application.name property value and word -destination.

spring-boot-istio-destinationrule

The name of created VirtualService is a concatenation of spring.application.name property value and word -route. Here’s the definition of VirtualService created for annotation @EnableIstio(version = "v1", timeout = 3, numberOfRetries = 3) and caller-service application.

spring-boot-istio-virtualservice

How it is implemented

We need to define a bean that implements BeanPostProcessor interface. On application startup it is trying to find the annotation @EnableIstio. If such annotation exists it takes a value of its fields and then it is creating new Istio objects or editing the currently existing objects.

public class EnableIstioAnnotationProcessor implements BeanPostProcessor {

    private final Logger LOGGER = LoggerFactory.getLogger(EnableIstioAnnotationProcessor.class);
    private ConfigurableListableBeanFactory configurableBeanFactory;
    private IstioClient istioClient;
    private IstioService istioService;

    public EnableIstioAnnotationProcessor(ConfigurableListableBeanFactory configurableBeanFactory, IstioClient istioClient, IstioService istioService) {
        this.configurableBeanFactory = configurableBeanFactory;
        this.istioClient = istioClient;
        this.istioService = istioService;
    }

    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        EnableIstio enableIstioAnnotation =  bean.getClass().getAnnotation(EnableIstio.class);
        if (enableIstioAnnotation != null) {
            LOGGER.info("Istio feature enabled: {}", enableIstioAnnotation);

            Resource<DestinationRule, DoneableDestinationRule> resource = istioClient.v1beta1DestinationRule()
                    .withName(istioService.getDestinationRuleName());
            if (resource.get() == null) {
                createNewDestinationRule(enableIstioAnnotation);
            } else {
                editDestinationRule(enableIstioAnnotation, resource);
            }

            Resource<VirtualService, DoneableVirtualService> resource2 = istioClient.v1beta1VirtualService()
                    .withName(istioService.getVirtualServiceName());
            if (resource2.get() == null) {
                 createNewVirtualService(enableIstioAnnotation);
            } else {
                editVirtualService(enableIstioAnnotation, resource2);
            }
        }
        return bean;
    }
   
}

We are using the API provided by Istio Client library. It provides a set of builders dedicated for creating elements of Istio objects.

private void createNewDestinationRule(EnableIstio enableIstioAnnotation) {
   DestinationRule dr = new DestinationRuleBuilder()
      .withMetadata(istioService.buildDestinationRuleMetadata())
      .withNewSpec()
      .withNewHost(istioService.getApplicationName())
      .withSubsets(istioService.buildSubset(enableIstioAnnotation))
      .withTrafficPolicy(istioService.buildCircuitBreaker(enableIstioAnnotation))
      .endSpec()
      .build();
   istioClient.v1beta1DestinationRule().create(dr);
   LOGGER.info("New DestinationRule created: {}", dr);
}

private void editDestinationRule(EnableIstio enableIstioAnnotation, Resource<DestinationRule, DoneableDestinationRule> resource) {
   LOGGER.info("Found DestinationRule: {}", resource.get());
   if (!enableIstioAnnotation.version().isEmpty()) {
      Optional<Subset> subset = resource.get().getSpec().getSubsets().stream()
         .filter(s -> s.getName().equals(enableIstioAnnotation.version()))
         .findAny();
      resource.edit()
         .editSpec()
         .addAllToSubsets(subset.isEmpty() ? List.of(istioService.buildSubset(enableIstioAnnotation)) :
                  Collections.emptyList())
            .editOrNewTrafficPolicyLike(istioService.buildCircuitBreaker(enableIstioAnnotation)).endTrafficPolicy()
         .endSpec()
         .done();
   }
}

private void createNewVirtualService(EnableIstio enableIstioAnnotation) {
   VirtualService vs = new VirtualServiceBuilder()
         .withNewMetadata().withName(istioService.getVirtualServiceName()).endMetadata()
      .withNewSpec()
      .addToHosts(istioService.getApplicationName())
      .addNewHttp()
      .withTimeout(enableIstioAnnotation.timeout() == 0 ? null : new Duration(0, (long) enableIstioAnnotation.timeout()))
      .withRetries(istioService.buildRetry(enableIstioAnnotation))
         .addNewRoute().withNewDestinationLike(istioService.buildDestination(enableIstioAnnotation)).endDestination().endRoute()
      .endHttp()
      .endSpec()
      .build();
   istioClient.v1beta1VirtualService().create(vs);
   LOGGER.info("New VirtualService created: {}", vs);
}

private void editVirtualService(EnableIstio enableIstioAnnotation, Resource<VirtualService, DoneableVirtualService> resource) {
   LOGGER.info("Found VirtualService: {}", resource.get());
   if (!enableIstioAnnotation.version().isEmpty()) {
      istioClient.v1beta1VirtualService().withName(istioService.getVirtualServiceName())
         .edit()
         .editSpec()
         .editFirstHttp()
         .withTimeout(enableIstioAnnotation.timeout() == 0 ? null : new Duration(0, (long) enableIstioAnnotation.timeout()))
         .withRetries(istioService.buildRetry(enableIstioAnnotation))
         .editFirstRoute()
         .withWeight(enableIstioAnnotation.weight() == 0 ? null: enableIstioAnnotation.weight())
            .editOrNewDestinationLike(istioService.buildDestination(enableIstioAnnotation)).endDestination()
         .endRoute()
         .endHttp()
         .endSpec()
         .done();
   }
}

The post Spring Boot Library for integration with Istio appeared first on Piotr's TechBlog.

]]>
https://piotrminkowski.com/2020/06/10/spring-boot-library-for-integration-with-istio/feed/ 2 8102