Tuesday, January 23, 2018

Apache Kafka - getting started. Simple java project.

0. Intro

Kafka® is used for building real-time data pipelines and streaming apps. It is horizontally scalable, fault-tolerant, wicked fast, and runs in production in thousands of companies.

From wiki
The project aims to provide a unified, high-throughput, low-latency platform for handling real-time data feeds. Its storage layer is essentially a "massively scalable pub/sub message queue architected as a distributed transaction log,"[3] making it highly valuable for enterprise infrastructures to process streaming data. Additionally, Kafka connects to external systems (for data import/export) via Kafka Connect and provides Kafka Streams, a Java stream processing library.
The design is heavily influenced by transaction logs.[4]


Last time Apache Kafka is getting more and more popular. With growing popularity of event-sourcing concept, more and more developers are switching to Kafka as primary storage of events. Kafka has everything for this: it's very hast, compact, scalable, "user-friendly"....
In this post I'll show basic simple operations like "send"(by producer) and "receive"(by consumer) messages.


1. Downloading and running kafka

This page is explaining very well how to download and run kafka. If you're using Windows, you can use next commands from "bin/windows" folder:

Run these commands from your Kafka root folder:
cd bin/windows
Then run Zookeper server:
zookeeper-server-start.bat ../../config/zookeeper.properties
Then run Kafka server:
kafka-server-start.bat ../../config/server.properties

Now when kafka is running you can check it by creating a topic and getting topic list:

Create a topic:
kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test0
List topics:
kafka-topics.bat --list --zookeeper localhost:2181

Response should be something like:
D:\Projects\kafka_2.11-1.0.0\bin\windows>kafka-topics.bat --list --zookeeper localhost:2181
test0


2. Project structure

Our project structure is very simple: we need just 2 files MessageProducer and MessageConsumer. 

build.gradle file:

group 'com.demien'version '1.0-SNAPSHOT'
apply plugin: 'java'
sourceCompatibility = 1.8
repositories {
    mavenCentral()
}

dependencies {
    compile 'org.apache.kafka:kafka-clients:0.9.0.0'    compile 'org.slf4j:slf4j-api:1.7.12'    compile 'org.slf4j:slf4j-log4j12:1.7.12'    compile 'log4j:log4j:1.2.17'

    testCompile group: 'junit', name: 'junit', version: '4.11'}



3. Producer

It designed as generic by KEY,VALUE types. Also I added to constructor optional messageSentCallback parameter - this callBack will be called when message was sent.


package com.demien.kafka;

import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;

import java.util.Date;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.function.Consumer;

public class MessageProducer<K, V> {

    private final Producer kafkaProducer;
    private final String topicName;
    private final Consumer<RecordMetadata> messageSentCallback;

    public MessageProducer(String topicName) {
        this(topicName, null);
    }

    public MessageProducer(String topicName, Consumer<RecordMetadata> messageSentCallback) {
        Properties configProperties = new Properties();
        configProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        configProperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArraySerializer");
        configProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        this.kafkaProducer = new KafkaProducer(configProperties);
        this.topicName = topicName;
        this.messageSentCallback = messageSentCallback;
    }


    public void sendMessage(K key, V value) {
        ProducerRecord<K, V> rec = new ProducerRecord<K, V>(topicName, key, value);
        Future<RecordMetadata> future = kafkaProducer.send(rec);
        if (messageSentCallback != null) {
            CompletableFuture.supplyAsync(() -> {
                try {
                    RecordMetadata recordMetadata = future.get();
                    messageSentCallback.accept(recordMetadata);
                } catch (Exception e) {
                }
                return null;
            });
        }
    }

    public void close() {
        kafkaProducer.close();
    }


    public static void main(String[] args) throws InterruptedException {
        MessageProducer<String, String> testProducer = new MessageProducer<String, String>("test0", (recordMetadata) -> {
            System.out.println("Message was sent: offset:" + recordMetadata.offset() + " partition:" + recordMetadata.partition() + " topic:" + recordMetadata.topic());
        });
        testProducer.sendMessage(null, "Test 1 " + new Date().toString());
        testProducer.sendMessage(null, "Test 2 " + new Date().toString());
        testProducer.sendMessage(null, "Test 3 " + new Date().toString());
        testProducer.close();
    }


}



4. Consumer

This class is more complicated, because it's designed to deal with the offsets for reading the data.
Consumer can start reading form the beginning, from the end, or from provided offset. That is why constructor is so complicated. Method for receiving messages is pretty simple. Supplier for cuncumed messages is provided in constructor.

package com.demien.kafka;

import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Properties;

import java.util.function.BiConsumer;

public class MessageConsumer<K, V> {
    private final String topic;
    private final String groupId;
    private final long startingOffset;
    private final KafkaConsumer<K, V> kafkaConsumer;

    public MessageConsumer(String topic, String groupId) {
        this(topic, groupId, -1);
    }

    /**     * @param topic - id of topic     * @param groupId - id of consumer group     * @param startingOffset - offset to read messages. 0 - from the beginning.      *                       -1 - from the end. other values - start reading from this value                            */    public MessageConsumer(String topic, String groupId, long startingOffset) {
        this.topic = topic;
        this.groupId = groupId;
        this.startingOffset = startingOffset;

        Properties configProperties = new Properties();
        configProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        configProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.ByteArrayDeserializer");
        configProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        configProperties.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
        configProperties.put(ConsumerConfig.CLIENT_ID_CONFIG, "testClient");
        configProperties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
        configProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");

        kafkaConsumer = new KafkaConsumer<>(configProperties);

        kafkaConsumer.subscribe(Arrays.asList(topic), new ConsumerRebalanceListener() {
            public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
                System.out.printf("%s topic-partitions are revoked from this consumer\n", Arrays.toString(partitions.toArray()));
            }

            public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
                System.out.printf("%s topic-partitions are assigned to this consumer\n", Arrays.toString(partitions.toArray()));
                Iterator<TopicPartition> topicPartitionIterator = partitions.iterator();
                while (topicPartitionIterator.hasNext()) {
                    TopicPartition topicPartition = topicPartitionIterator.next();
                    System.out.println("Current offset is " + kafkaConsumer.position(topicPartition) + " committed offset is ->" + kafkaConsumer.committed(topicPartition));
                    if (MessageConsumer.this.startingOffset == 0) {
                        System.out.println("Setting offset to begining");

                        kafkaConsumer.seekToBeginning(topicPartition);
                    } else if (MessageConsumer.this.startingOffset == -1) {
                        System.out.println("Setting it to the end ");

                        kafkaConsumer.seekToEnd(topicPartition);
                    } else {
                        System.out.println("Resetting offset to " + MessageConsumer.this.startingOffset);
                        kafkaConsumer.seek(topicPartition, MessageConsumer.this.startingOffset);
                    }
                }
            }
        });

    }

    public void startReceiving(BiConsumer<K, V> biConsumer) {
        try {
            while (true) {
                ConsumerRecords<K, V> records = kafkaConsumer.poll(100);
                records.forEach(record->  biConsumer.accept(record.key(), record.value()));
                if (startingOffset == -2) kafkaConsumer.commitSync();
            }
        } finally {
            kafkaConsumer.close();
        }
    }

    public static void main(String[] args) {
        final MessageConsumer<String, String> testConsumer = new MessageConsumer<>("test0", "testGroup");
        testConsumer.startReceiving( (k,v) -> System.out.println("received:"+v) );

    }
}


5. Execution 

Let's start the Consumer now. It should output something like:

[test0-0] topic-partitions are assigned to this consumer
Current offset is 0 committed offset is ->null


Now let's start the Producer. Is should send 3 test messages and print information about them:

Message was sent: offset:0 partition:0 topic:test0
Message was sent: offset:1 partition:0 topic:test0
Message was sent: offset:2 partition:0 topic:test0

Consumer also should print information about received messages:

received:Test 1 Tue Jan 23 15:36:29 CET 2018
received:Test 2 Tue Jan 23 15:36:29 CET 2018
received:Test 3 Tue Jan 23 15:36:29 CET 2018

Let's restart our consumer now. By default value in our constructor, if will be reading data from the end, so previous messages will not be shown:

[test0-0] topic-partitions are assigned to this consumer
Current offset is 3 committed offset is ->OffsetAndMetadata{offset=3, metadata=''}
Setting it to the end 


Now we can try to read previous messages by changing the constructor parameter:

public static void main(String[] args) {
    final MessageConsumer<String, String> testConsumer = new MessageConsumer<>("test0", "testGroup", 2);
    testConsumer.startReceiving( (k,v) -> System.out.println("received:"+v) );
}

- it's now 2 so we will be reading from offset 2. Let's restart it again:

[test0-0] topic-partitions are assigned to this consumer
Current offset is 3 committed offset is ->OffsetAndMetadata{offset=3, metadata=''}
Resetting offset to 2
received:Test 3 Tue Jan 23 15:36:29 CET 2018

- now last previous message with the offset 2 was read.

6. The end

Source code can be downloaded from here.

Thursday, November 9, 2017

Completable future (java 8) simple example

A long long time ago class Future was introduced in Java. Now, with Java 8 we have an "upgraded" version of it.

1. Regular "Future"

Futures are very useful objects: if some task which will be executed in another thread have to return some value - we can use Callable object which returns Futubre object. Later we will be able to get the value returned by task by executing .get method from returned Future. The problem here: when we are executing .get - we are blocked: if the value is not ready yet(long running task) we will be waiting till the value will finally be returned.

Let's write some code to recall our knowledge of Future:

Function for simulation of long running task:

public int longTask(int delay) {
    try {
        Thread.sleep(delay);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return delay;

}

Function for creation Callable from this long task:
public Callable<Integer> longCallable(int delay) {
    Callable<Integer> result = () -> longTask(delay);
    return result;
}


Function for submitting Callable by Executor and creation of Future:

public Future<Integer> longFuture(int delay) {
    return Executors.newWorkStealingPool().submit(longCallable(delay));
}

Debug function to print our Futures:

public void printFuture(Future<Integer> future) {
    try {
        System.out.println(future.get());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

Let's create an array with several futures:

public List<Future<Integer>> getTestFutures() {
    return IntStream.rangeClosed(1, 3)
            .<Future<Integer>>mapToObj(i -> longFuture(i))
            .collect(Collectors.toList());
}

Now we have a Futures, so let's take the values from them:  

public void blockingFutureGet() {
    getTestFutures().forEach(f -> printFuture(f));
}


And, as we mentioned before - we are blocked here. 
After next execution:  

System.out.println("Begin");
app.blockingFutureGet();
System.out.println("End");


Result will be: 
Begin
1
2
3
End

So, the main thread was blocked till we finally got ALL VALUES from the Futures. 
Sometimes such behavior is not acceptable: we may need to execute tasks in totally async way. 
For that purpose we may rewrite previous function this way: 


public void nonBlockingFutureGet() {
    new Thread(this::blockingFutureGet).start();
}

We are running additional thread to get values from the Futures.
The result will be:
Begin
End
1
2
3

-  the main thread was not blocked - it continued execution while another thread was getting values from Futures. But this approach little bit complicated: we have to submit our tasks, collect the futures, run additional thread, pass collected futures there and execute all needed manipulations. To make it simpler we can just use CompletableFutures.

2. CompletableFuture

And now let's compare all the code above with CompletableFuture implementation:

public void completableFuture() {
    IntStream.rangeClosed(1, 3).forEach(i ->
        CompletableFuture.supplyAsync(() -> longTask(i))
           .thenAccept(System.out::println)
    );
}

It's now much easier!  Also results can be forwarded further for additional tasks/operations:

public void completableFuture2() {
    IntStream.rangeClosed(1, 3).forEach(i ->
            CompletableFuture.supplyAsync(() -> longTask(i))
                    .thenApply(v->v*10)
                    .thenApply(v->Integer.toString(v)+"!")
                    .thenAccept(System.out::println)
                    .exceptionally(e-> {
                        System.out.println("We have a problem:"+e.getMessage());
                        return null;
                    })
    );
}

3. Running additional asynchronous tasks using thenCompose

In previous example we used thenApply method which executes tasks in synchronous way. But it can be asynchronous - we can execute thenCompose method and return another async task:

public void cfThenCompose() {
    IntStream.rangeClosed(1, 3)
            .mapToObj(i -> CompletableFuture.supplyAsync(() -> longTask(i * 1000)))
            .forEach(f -> f.thenCompose(j -> {
                             System.out.println(j);
                             return CompletableFuture.supplyAsync(() -> longTask(j + 1));
                         }).thenAccept(System.out::println)
                     );
}


Results of execution:
1000
2000
1001
3000
2001
3001

4. Combining results of 2 independent futures.

In a previous example we were running execution based on previous execution.But what if need result of 2 tasks for further computation? We can use thenCombine method or that:

public void cfThenCombine() {
    CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(()->longTask(1000));
    CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(()->longTask(2000));
    f1.thenCombine(f2, (r1, r2)-> r1+r2).thenAccept(System.out::println);
}

The result is, as expected:
3000

5. The end.

CompletableFutures as the big step forward for java in the direction of functional programming. 

Sunday, October 29, 2017

Spring bean lifecycle, postrocessors, profiler

0. intro

When we create object - we have only one entry point to modify somehow object state: constructor.
In Spring this process is much more complicated:


let's create a simple spring application to see how it goes.

1. Project structure

It's a regular gradle-java project.

conten of buid.gradle file:

group 'com.demien.spring'version '1.0-SNAPSHOT'
apply plugin: 'java'
sourceCompatibility = 1.8
repositories {
    mavenCentral()
}

dependencies {
    compile group: 'org.springframework', name: 'spring-core', version: '5.0.0.RELEASE'    
    compile group: 'org.springframework', name: 'spring-beans', version: '5.0.0.RELEASE'    compile group: 'org.springframework', name: 'spring-context', version: '5.0.0.RELEASE'
    testCompile group: 'junit', name: 'junit', version: '4.12'}



directory structure:




2. Annotations

For deep understanding of some life cycle phases let's create 2 annotations:

- Generate name annotation. If field is annotated by this annotation, that means we have to generate the value for this field - simulation of "name". For that we have also settings: minLenght and maxLenght  - length of name which we have to generate.

package com.demien.spring.lifecycle.annotations;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface GeneratedName {
    int minLength();
    int maxLength();
}

- Profiling annotation. If class has such annotation - we have to measure execution time of every class method.
package com.demien.spring.lifecycle.annotations;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Profiling {
}


3. Spring bean - messenger

Now let's create a very simple bean which will be printing messages.

Interface is very simple: just 2 methods:

package com.demien.spring.lifecycle.beans;

public interface Messenger {
    void printMessage();
    void setUp(String symbol);
}


Implementation is more complicated:
  - we are using annotations from above
  - we have 3 "state" methods: constructor, init and setup


package com.demien.spring.lifecycle.beans;

import com.demien.spring.lifecycle.annotations.GeneratedName;
import com.demien.spring.lifecycle.annotations.Profiling;

import java.time.LocalTime;

@Profiling
public class SimpleMessenger implements Messenger {

    private String messageText = "NULL";
    private String symbol = "";

    @GeneratedName(minLength = 5, maxLength = 10)
    private String name;

    public SimpleMessenger() {
        System.out.print("Constructor: ");
        printMessage();
    }

    public void init() {
        System.out.print("Init: ");
        printMessage();
    }

    @Override    
    public void setUp(String symbol) {
        this.symbol = symbol;
        System.out.print("Setup: ");
        printMessage();
    }
public void setMessageText(String messageText) { this.messageText = messageText; } @Override
    public void printMessage() {
        System.out.println();
        System.out.println(messageText + ", " + name+symbol+" ["+ LocalTime.now()+"]");
    }


}


4. JMX profiler settings 

For profiler, it's better to have ability to turn on/off when it's needed. We can use JMX for that.
For JMX we need an interface which name ends by MBean and implementation of it:

package com.demien.spring.lifecycle.jmx;

public interface ProfilerSettingsMBean {
    void setEnabled(boolean enabled);
}


package com.demien.spring.lifecycle.jmx;

public class ProfilerSettings implements ProfilerSettingsMBean {

    private boolean enabled;

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }
}


5. AppConfig

It's the most complicated and interesting part of our application.

- definition of message bean. Here we also defining "init-method" which will be executed after bean initialization.

@Bean(initMethod = "init")
Messenger messenger() {
    SimpleMessenger messenger = new SimpleMessenger();
    messenger.setMessageText("Hello");
    return messenger;
}
-

- We want to do some actions(execute messenger.setUp method) when spring created application context - on context refresh event.

@Bean
ApplicationListener<ContextRefreshedEvent> refreshedEventApplicationListener() {
    return new ApplicationListener<ContextRefreshedEvent>() {
        @Override        
        public void onApplicationEvent(ContextRefreshedEvent event) {
            ApplicationContext ctx = event.getApplicationContext();
            Messenger messenger = ctx.getBean(Messenger.class);
            messenger.setUp("!");
        }
    };

}

- we need post processor for dealing with annotation @GeneratedName - we have to generate name and put it into such field.

@Bean
BeanPostProcessor nameGenerationPostProcessor() {

    return new BeanPostProcessor() {
        @Override        
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            Field[] fields = bean.getClass().getDeclaredFields();
            for (Field field : fields) {
                GeneratedName annotation = field.getAnnotation(GeneratedName.class);
                if (annotation != null) {
                    field.setAccessible(true);
                    ReflectionUtils.setField(field, bean, generateName(annotation.minLength(), annotation.maxLength()));
                }
            }
            return bean;
        }

    };
}


- and also we have to process @Profiling annotation.
For that we can on "beforeInitalization" phase store classes which have such annotation. And on "afterInilialization" phase we can return "proxy" object which will be printing execution time after method invocation if such settings is turned on.

@Bean
BeanPostProcessor profilingPostProcessor() throws Exception {
    Map<String, Class> map = new HashMap<>();
    ProfilerSettings profilerSettings = new ProfilerSettings();
    MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer();
    beanServer.registerMBean(profilerSettings, new ObjectName("Profiling", "name", "settings"));

    return new BeanPostProcessor() {

        @Override        
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            Class<?> beanClass = bean.getClass();
            if (beanClass.isAnnotationPresent(Profiling.class)) {
                map.put(beanName, beanClass);
            }
            return null;
        }

        @Override        
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            Class beanClass = map.get(beanName);
            if (beanClass != null) {
                return Proxy.newProxyInstance(beanClass.getClassLoader(), beanClass.getInterfaces(), new InvocationHandler() {
                    @Override                    public Object invoke(Object proxy, Method method, Object[] objects) throws Throwable {
                        long before = System.nanoTime();

                        Object retval = method.invoke(bean, objects);
                        if (profilerSettings.isEnabled()) {
                            System.out.println("exec time:" + (System.nanoTime() - before));
                        }
                        return retval;
                    }
                });
            }
            return null;
        }
    };
}

6. Main app class

Here we just creating sprint context and executing in a loop printMessage method of our messenger.

package com.demien.spring.lifecycle;

import com.demien.spring.lifecycle.beans.Messenger;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App {

    public static void main(String[] args) throws InterruptedException {

        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.register(AppConfig.class);
        ctx.refresh();
        Messenger messenger = ctx.getBean(Messenger.class);

        System.out.println();
        System.out.println("Execution started");
        while (true) {
            Thread.sleep(3000);
            messenger.printMessage();
        }

    }
}


7. Execution

Now we can run our application:

Constructor: 
NULL, null [21:27:53.542]
Init: 
Hello, Mosxrak [21:27:53.553]
Setup: 
Hello, Mosxrak! [21:27:53.678]

Execution started

Hello, Mosxrak! [21:27:56.682]

Hello, Mosxrak! [21:27:59.683]

Hello, Mosxrak! [21:28:02.683]

Here we can see all phases of bean lifecycle in spring:
- first of all, of course, constructor is executed. On this phase nothing was injected into our bean, that is why we have nulls.
- init method was executed when spring created the bean. So bean has message and also post processor generated name
- the last method is "setup" - it was executed when the whole context was created and refreshed. 

8. Turning on profiling 

Now let's test how profiling works. We have to connect JMX client (for example JConsole, or VisualVM) and connect it to our application.



In Profiling/Settings we can see our flag "enaled" - here we have to enter "true" and press "enter".
After this operation output will be changed:


Hello, Cswwjksic! [21:37:28.926]

Hello, Cswwjksic! [21:37:31.926]

Hello, Cswwjksic! [21:37:34.926]

Hello, Cswwjksic! [21:37:37.928]
exec time:1931750

Hello, Cswwjksic! [21:37:40.929]
exec time:235493

Hello, Cswwjksic! [21:37:43.929]
exec time:184623

Hello, Cswwjksic! [21:37:46.930]
exec time:340191

Hello, Cswwjksic! [21:37:49.930]
exec time:358737


After turning on our profiler prints execution time - so everything works as expected.


9. The end

Full source code can be downloaded from here

Wednesday, September 27, 2017

Swagger with SpringBoot

From Wiki:
Swagger is an open source software framework backed by a large ecosystem of tools that helps developers design, build, document, and consume RESTful Web services. While most users identify Swagger by the Swagger UI tool, the Swagger toolset includes support for automated documentation, code generation, and test case generation.

Official page: https://swagger.io/


1. What is Swagger?

In this post I'll show 2 components of Swagger: 
   - set of annotations which help us to "describe" REST - related stuff: rest endpoints and DTO-objects.
   - Swagger UI, which can be used for calling this endpoints for testing purposes.

Example of "description" of DTO-object filed:   
@ApiModelProperty(notes = "Group Name")
private String name;

Example of "description" of REST-endpoint:
@GET@Path("/{id}")
@ApiOperation(value = "Get group by id resource.", response = Group.class)
@ApiResponses(value = {
        @ApiResponse(code = 200, message = "Group resource found"),
        @ApiResponse(code = 404, message = "Group resource not found")
})
public Response getGroup(@ApiParam @PathParam("id") Long id) {

Example of Swagger UI, using which we can call just listed above method: 




2. build.gradle 

I just generated the SpringBoot project from start.spring.io and added swagger dependency into it:
buildscript {
   ext {
      springBootVersion = '1.5.7.RELEASE'   }
   repositories {
      mavenCentral()
   }
   dependencies {
      classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
   }
}

apply plugin: 'java'apply plugin: 'eclipse'apply plugin: 'org.springframework.boot'
jar.archiveName = "SwaggerTestApp.jar"group = 'com.demien'version = '0.0.1-SNAPSHOT'sourceCompatibility = 1.8
repositories {
   mavenCentral()
}


dependencies {
   compile('org.springframework.boot:spring-boot-starter-jersey')
   compile('org.springframework.boot:spring-boot-starter-web')

    compile group: 'io.swagger', name: 'swagger-jersey2-jaxrs', version: '1.5.16'       testCompile('org.springframework.boot:spring-boot-starter-test')
}



3. Main start class


Nothing special here, just  adding several packages for scanning

package com.demien.swtest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(
    scanBasePackages = {
         "com.demien.swtest.config", 
         "com.demien.swtest.rest", 
         "com.demien.swtest.service"         }
)
public class SwtestApplication  {

   public static void main(String[] args) {
      SpringApplication.run(SwtestApplication.class, args);
   }
}



4. Jersey config


It's the most complicated part of application - we have to configure swagger here with metha-data of our application. 


package com.demien.swtest.config;

import com.demien.swtest.rest.GroupResource;
import io.swagger.jaxrs.config.BeanConfig;
import io.swagger.jaxrs.listing.ApiListingResource;
import io.swagger.jaxrs.listing.SwaggerSerializers;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.wadl.internal.WadlResource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

    @Component    public class JerseyConfig extends ResourceConfig {

        @Value("${spring.jersey.application-path:/}")
        private String apiPath;

        public JerseyConfig() {
            // Register endpoints, providers, ...            this.registerEndpoints();
        }

        @PostConstruct        public void init() {
            // Register components where DI is needed            this.configureSwagger();
            //this.registerEndpoints();        }

        private void registerEndpoints() {
            this.register(GroupResource.class);
            // Access through /<Jersey's servlet path>/application.wadl            this.register(WadlResource.class);
        }

        private void configureSwagger() {
            // Available at localhost:port/api/swagger.json            this.register(ApiListingResource.class);
            this.register(SwaggerSerializers.class);

            BeanConfig config = new BeanConfig();
            config.setConfigId("springboot-jersey-swagger-test-app");
            config.setTitle("Spring Boot, Jersey, Swagger Test Application");
            config.setVersion("v1");
            config.setContact("Dmitry Kovalsky");
            config.setSchemes(new String[] { "http", "https" });
            config.setBasePath(this.apiPath);
            config.setResourcePackage("com.demien.swtest.rest");
            config.setPrettyPrint(true);
            config.setScan(true);
        }
}


5. Dto and Model classes

UI is not sending ID - it will be generated on server side, what is why I need 2 classes: one for data which will be sent from UI and the second one  - for response. Fields in these classes are swagger-annotated.


package com.demien.swtest.dto;

import io.swagger.annotations.ApiModelProperty;

public class GroupDTO {

    @ApiModelProperty(notes = "Group Name")
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

package com.demien.swtest.model;

import com.demien.swtest.dto.GroupDTO;
import io.swagger.annotations.ApiModelProperty;

public class Group extends GroupDTO{
    @ApiModelProperty(notes = "Generated Group ID")
    private Long id;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Group() {
    }

    public Group(GroupDTO dto) {
        setName(dto.getName());
    }

}



6. Rest controller(resource)

Here, all rest methods are swagger-annotated with description and errors which may be raised by it. 


package com.demien.swtest.rest;

import com.demien.swtest.dto.GroupDTO;
import com.demien.swtest.model.Group;
import com.demien.swtest.service.GroupService;
import io.swagger.annotations.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

@Component@Path("/groups")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Api(value = "Group resource", produces = "application/json")
public class GroupResource {

    @Autowired    private GroupService groupService;

    public Response OkResponse(Object entity) {
        return Response.status(Response.Status.OK).entity(entity).build();
    }

    public Response NotFoundResponse() {
        return Response.status(Response.Status.NOT_FOUND).build();
    }

    @POST    @ApiOperation(value = "Create group.", response = Group.class)
    @ApiResponses(value = {
            @ApiResponse(code = 201, message = "group resource ", responseHeaders = {
                    @ResponseHeader(name = "Location", description = "The URL to retrieve created resource", response = String.class)
            })
    })
    public Response createGroup(GroupDTO groupDTO, @Context UriInfo uriInfo) {
        Group result = groupService.add(new Group(groupDTO));
        return OkResponse(result);
    }

    @GET    @Path("/{id}")
    @ApiOperation(value = "Get group by id resource.", response = Group.class)
    @ApiResponses(value = {
            @ApiResponse(code = 200, message = "Group resource found"),
            @ApiResponse(code = 404, message = "Group resource not found")
    })
    public Response getGroup(@ApiParam @PathParam("id") Long id) {
        Group result = groupService.get(id);
        return result == null ? NotFoundResponse() : OkResponse(result);
    }

    @PUT    @ApiOperation(value = "Update group.", response = Group.class)
    @ApiResponses(value = {
            @ApiResponse(code = 200, message = "Group resource found")
    })
    public Response updateGroup(Group group, @Context UriInfo uriInfo) {
        groupService.update(group.getId(), group);
        Group result = groupService.get(group.getId());
        return OkResponse(result);
    }

    @DELETE    @Path("/{id}")
    @ApiOperation(value = "Delete group by id resource.")
    @ApiResponses(value = {
            @ApiResponse(code = 200, message = "Group resource found"),
            @ApiResponse(code = 404, message = "Group resource not found")
    })
    public Response deleteGroup(@ApiParam @PathParam("id") Long id) {
        Group result = groupService.get(id);
        if (result == null) return NotFoundResponse();
        groupService.delete(id);
        return OkResponse(id);
    }


}      

7. Imitation of service
I made imitation of generic service and concrete subclass. 


package com.demien.swtest.service;

import com.demien.swtest.model.Group;
import org.springframework.stereotype.Service;

import java.util.function.UnaryOperator;

@Servicepublic class GroupService extends AbstractService<Group> {

    public GroupService() {
        super((e, id) -> {
            e.setId(id);
            return e;
        });
    }
}



package com.demien.swtest.service;

import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.UnaryOperator;

public abstract class AbstractService<T> {

    private long id;
    private Map<Long,T> storage = new HashMap<>();
    private BiFunction<T, Long, T> idSetter;

    public AbstractService(BiFunction<T, Long, T> idSetter) {
        this.idSetter = idSetter;
    }


    public T add(T entity) {
        id++;
        T result =  idSetter.apply(entity, id);
        storage.put(id, result);
        return result;

    }

    public T get(Long id) {
        return storage.get(id);
    }

    public void update(Long id, T entity) {
        storage.put(id, entity);
    }

    public void delete(Long id) {
        storage.put(id, null);
    }
}



8. swagger.json

Now we can run our application and open this address: http://localhost:8080/api/swagger.json
The result should be - json generated by swagger which "explains" our rest endpoints with 
detailed description: 

{
   "swagger":"2.0",
   "info":{
      "version":"v1",
      "title":"Spring Boot, Jersey, Swagger Test Application",
      "contact":{
         "name":"Dmitry Kovalsky"
      }
   },
   "basePath":"/api",
   "tags":[
      {
         "name":"Group resource"
      }
   ],
   "schemes":[
      "http",
      "https"
   ],
   "paths":{
      "/groups/{id}":{
         "get":{
            "tags":[
               "Group resource"
            ],
            "summary":"Get group by id resource.",
            "description":"",
            "operationId":"getGroup",
            "consumes":[
               "application/json"
            ],
            "produces":[
               "application/json"
            ],
            "parameters":[
               {
                  "name":"id",
                  "in":"path",
                  "required":true,
                  "type":"integer",
                  "format":"int64"
               }
            ],
            "responses":{
               "200":{
                  "description":"Group resource found"
               },
               "404":{
                  "description":"Group resource not found"
               }
            }
         },
         "delete":{
            "tags":[
               "Group resource"
            ],
            "summary":"Delete group by id resource.",
            "description":"",
            "operationId":"deleteGroup",
            "consumes":[
               "application/json"
            ],
            "produces":[
               "application/json"
            ],
            "parameters":[
               {
                  "name":"id",
                  "in":"path",
                  "required":true,
                  "type":"integer",
                  "format":"int64"
               }
            ],
            "responses":{
               "200":{
                  "description":"Group resource found"
               },
               "404":{
                  "description":"Group resource not found"
               }
            }
         }
      },
      "/groups":{
         "post":{
            "tags":[
               "Group resource"
            ],
            "summary":"Create group.",
            "description":"",
            "operationId":"createGroup",
            "consumes":[
               "application/json"
            ],
            "produces":[
               "application/json"
            ],
            "parameters":[
               {
                  "in":"body",
                  "name":"body",
                  "required":false,
                  "schema":{
                     "$ref":"#/definitions/GroupDTO"
                  }
               }
            ],
            "responses":{
               "200":{
                  "description":"successful operation",
                  "schema":{
                     "$ref":"#/definitions/Group"
                  }
               },
               "201":{
                  "description":"group resource ",
                  "headers":{
                     "Location":{
                        "type":"string",
                        "description":"The URL to retrieve created resource"
                     }
                  }
               }
            }
         },
         "put":{
            "tags":[
               "Group resource"
            ],
            "summary":"Update group.",
            "description":"",
            "operationId":"updateGroup",
            "consumes":[
               "application/json"
            ],
            "produces":[
               "application/json"
            ],
            "parameters":[
               {
                  "in":"body",
                  "name":"body",
                  "required":false,
                  "schema":{
                     "$ref":"#/definitions/Group"
                  }
               }
            ],
            "responses":{
               "200":{
                  "description":"Group resource found"
               }
            }
         }
      }
   },
   "definitions":{
      "Group":{
         "type":"object",
         "properties":{
            "name":{
               "type":"string"
            },
            "id":{
               "type":"integer",
               "format":"int64"
            }
         }
      },
      "GroupDTO":{
         "type":"object",
         "properties":{
            "name":{
               "type":"string"
            }
         }
      }
   }
}


9. Swagger UI 

JSON with endpoint description - is great! But swagger can even more: based on this JSON, it can provide the UI to call these endpoints. We just have to download it from https://swagger.io/swagger-ui/ and put into src/main/resource/static. In will be available by address: http://localhost:8080/index.html.

Example of trying POST method on GROUP resource:


And after pressing "Try it out!" we will have: 



10. The end


Full source code can be downloaded from here