#149 - Add Redis repository support sample.

Bumped Spring Data Redis version to 1.7 RC1. Added JUnit Rule for Embedded Redis Server.

Original pull request: #162.
This commit is contained in:
Christoph Strobl
2016-02-08 08:59:27 +01:00
committed by Oliver Gierke
parent 1f94bb12f1
commit 5a25e80bbf
13 changed files with 765 additions and 0 deletions

View File

@@ -34,6 +34,7 @@ We have separate folders for the samples of individual modules:
* `example` - Example for basic Spring Data Redis setup.
* `cluster-sentinel` - Example for Redis cluster and Sentinel support.
* `repository` - Example demonstrating Spring Data repository abstraction on top of Redis.
## Spring Data Elasticsearch

View File

@@ -20,6 +20,7 @@
<module>cluster-sentinel</module>
<module>example</module>
<module>cluster</module>
<module>repository</module>
</modules>
<dependencies>

124
redis/repository/README.md Normal file
View File

@@ -0,0 +1,124 @@
# Spring Data Redis - Repository Examples #
This project contains examples for Spring Data specific repository abstraction on top of Redis.
## Repository Suport ##
Redis Repository support allows to convert, store, retrieve and index entities within Redis native data structures. To do, besides the `HASH` containing the actual properties several [Secondary Index](http://redis.io/topics/indexes) structures are set up and maintained.
```java
@RedisHash("persons")
class Person {
@Id String id;
@Indexed String firstname;
@Indexed String lastname;
Gender gender;
Address address;
@Reference List<Person> children;
}
```
The above entity would for example then be stored in a Redis [HASH](http://redis.io/topics/data-types#hashes) with key `persons:9b0ed8ee-14be-46ec-b5fa-79570aadb91d`.
```properties
_class=example.springdata.redis.domain.Person <1>
id=9b0ed8ee-14be-46ec-b5fa-79570aadb91d
firstname=eddard <2>
lastname=stark
gender=MALE
address.city=winterfell <3>
address.country=the north
children.[0]=persons:41436096-aabe-42fa-bd5a-9a517fbf0260 <4>
children.[1]=persons:1973d8e7-fbd4-4f93-abab-a2e3a00b3f53
children.[2]=persons:440b24c6-ede2-495a-b765-2d8b8d6e3995
```
```
<1> The _class attribute is used to store the actual type and is required for object/hash conversion.
<2> Values are also included in Secondary Index when annotated with @Indexed.
<3> Complex types are flattened out and embedded into the HASH as long as there is no explicit Converter registered or a @Reference annotation present.
<4> Using @Reference stores only the key of a referenced object without embedding values like in <3>.
```
Additionally indexes are created for `firstname`, `lastname` and `address.city` containing the `id` of the actual entity.
```bash
redis/src $ ./redis-cli keys *
1) "persons" <1>
2) "persons:9b0ed8ee-14be-46ec-b5fa-79570aadb91d" <2>
3) "persons:9b0ed8ee-14be-46ec-b5fa-79570aadb91d:idx" <3>
4) "persons:41436096-aabe-42fa-bd5a-9a517fbf0260"
5) "persons:41436096-aabe-42fa-bd5a-9a517fbf0260:idx"
6) "persons:1973d8e7-fbd4-4f93-abab-a2e3a00b3f53"
7) "persons:1973d8e7-fbd4-4f93-abab-a2e3a00b3f53:idx"
8) "persons:440b24c6-ede2-495a-b765-2d8b8d6e3995"
9) "persons:440b24c6-ede2-495a-b765-2d8b8d6e3995:idx"
10) "persons:firstname:eddard" <4>
11) "persons:firstname:robb"
12) "persons:firstname:sansa"
13) "persons:firstname:arya"
14) "persons:lastname:stark" <5>
15) "persons:address.city:winterfell" <6>
```
```
<1> SET holding all ids known in the keyspace "persons".
<2> HASH holding property values for id "9b0ed8ee-14be-46ec-b5fa-79570aadb91d" in keyspace "persons".
<3> SET holding index information for CRUD operations.
<4> SET used for indexing entities with "firstname" equal to "eddard" within keyspace "persons".
<5> SET used for indexing entities with "lastname" equal to "stark" within keyspace "persons".
<6> SET used for indexing entities with "address.city" equal to "winterfell" within keyspace "persons".
```
## Configuration ##
The below configuration uses [Jedis](https://github.com/xetorthio/jedis) to connect to Redis on its default port. Please note the usage of `@EnableRedisRepositories` to create `Repository` instances.
```java
@Configuration
@EnableRedisRepositories
class AppConfig {
@Bean
RedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
@Bean
RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
template.setConnectionFactory(connectionFactory);
return template;
}
}
```
Having the infrastructure in place you can go on declaring and using the `Repository` interface.
```java
interface PersonRepository extends CrudRepository<Person, String> {
List<Person> findByLastname(String lastname);
Page<Person> findByLastname(String lastname, Pageable page);
List<Person> findByFirstnameAndLastname(String firstname, String lastname);
List<Person> findByFirstnameOrLastname(String firstname, String lastname);
List<Person> findByAddress_City(String city);
}
```
## One Word of Caution ##
Maintaining complex types and index structures stands and falls with its usage. Please consider the following:
* Nested document structures increase object <> hash conversion overhead.
* Consider having only those indexes you really need instead of indexing each and every property.
* Pagination is a costly operation since the total number of elements is calculated before returning just a slice of data.
* Pagination gives no guarantee on information as elements might be added, moved or removed.

50
redis/repository/pom.xml Normal file
View File

@@ -0,0 +1,50 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-data-redis-repository-example</artifactId>
<name>Spring Data Redis - Repository Support Example</name>
<parent>
<groupId>org.springframework.data.examples</groupId>
<artifactId>spring-data-redis-examples</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-data-redis-example-utils</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.kstyrc</groupId>
<artifactId>embedded-redis</artifactId>
<version>0.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.7.0.RC1</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-keyvalue</artifactId>
<version>1.1.0.RC1</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>1.12.0.RC1</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.redis.domain;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.data.redis.core.index.Indexed;
/**
* @author Christoph Strobl
*/
@Data
@EqualsAndHashCode
class Address {
private @Indexed String city;
private String country;
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.redis;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
/**
* @author Christoph Strobl
*/
@Configuration
@EnableRedisRepositories
public class AppConfig {
/**
* {@link RedisConnectionFactory} to be used when connecting to redis.
*
* @return
*/
@Bean
RedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
/**
* @param connectionFactory
* @return
*/
@Bean
RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
template.setConnectionFactory(connectionFactory);
return template;
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.redis.domain;
/**
* @author Christoph Strobl
*/
enum Gender {
FEMALE, MALE
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.redis.domain;
import java.lang.reflect.Field;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Reference;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;
/**
* {@link Person} object stored inside a Redis {@literal HASH}. <br />
* <br />
* Sample (key = persons:9b0ed8ee-14be-46ec-b5fa-79570aadb91d):
*
* <pre>
* <code>
* _class := example.springdata.redis.domain.Person
* id := 9b0ed8ee-14be-46ec-b5fa-79570aadb91d
* firstname := eddard
* lastname := stark
* gender := MALE
* address.city := winterfell
* address.country := the north
* children.[0] := persons:41436096-aabe-42fa-bd5a-9a517fbf0260
* children.[1] := persons:1973d8e7-fbd4-4f93-abab-a2e3a00b3f53
* children.[2] := persons:440b24c6-ede2-495a-b765-2d8b8d6e3995
* children.[3] := persons:85f0c1d1-cef6-40a4-b969-758ebb68dd7b
* children.[4] := persons:73cb36e8-add9-4ec0-b5dd-d820e04f44f0
* children.[5] := persons:9c2461aa-2ef2-469f-83a2-bd216df8357f
* </code>
* </pre>
*
* @author Christoph Strobl
*/
@Data
@EqualsAndHashCode(exclude = { "children" })
@RedisHash("persons")
class Person {
/**
* The {@literal id} and {@link RedisHash#toString()} build up the {@literal key} for the Redis {@literal HASH}. <br />
*
* <pre>
* <code>
* {@link RedisHash#value()} + ":" + {@link Person#id}
* //eg. persons:9b0ed8ee-14be-46ec-b5fa-79570aadb91d
* </code>
* </pre>
*
* <strong>Note:</strong> empty {@literal id} fields are automatically assigned during save operation.
*/
private @Id String id;
/**
* Using {@link Indexed} marks the property as for indexing which uses Redis {@literal SET} to keep track of
* {@literal ids} for objects with matching values. <br />
*
* <pre>
* <code>
* {@link RedisHash#value()} + ":" + {@link Field#getName()} +":" + {@link Field#get(Object)}
* //eg. persons:firstname:eddard
* </code>
* </pre>
*/
private @Indexed String firstname;
private @Indexed String lastname;
private Gender gender;
/**
* Since {@link Indexed} is used on {@link Address#getCity()} index structures for {@code persons:address:city} are be
* maintained.
*/
private Address address;
/**
* Using {@link Reference} allows to link to existing objects via their {@literal key}. The values stored in the
* objects {@literal HASH} looks like:
*
* <pre>
* <code>
* children.[0] := persons:41436096-aabe-42fa-bd5a-9a517fbf0260
* children.[1] := persons:1973d8e7-fbd4-4f93-abab-a2e3a00b3f53
* children.[2] := persons:440b24c6-ede2-495a-b765-2d8b8d6e3995
* </code>
* </pre>
*/
private @Reference List<Person> children;
public Person() {}
public Person(String firstname, String lastname, Gender gender) {
this.firstname = firstname;
this.lastname = lastname;
this.gender = gender;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.redis.domain;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.CrudRepository;
/**
* @author Christoph Strobl
*/
interface PersonRepository extends CrudRepository<Person, String> {
List<Person> findByLastname(String lastname);
Page<Person> findPersonByLastname(String lastname, Pageable page);
List<Person> findByFirstnameAndLastname(String firstname, String lastname);
List<Person> findByFirstnameOrLastname(String firstname, String lastname);
List<Person> findByAddress_City(String city);
}

View File

@@ -0,0 +1,216 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.redis.domain;
import static org.hamcrest.collection.IsIterableContainingInAnyOrder.*;
import static org.hamcrest.core.Is.*;
import static org.hamcrest.core.IsCollectionContaining.*;
import static org.hamcrest.core.IsNot.*;
import static org.junit.Assert.*;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.index.Indexed;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import example.springdata.redis.AppConfig;
import example.springdata.redis.test.util.EmbeddedRedisServer;
import example.springdata.redis.test.util.RequiresRedisServer;
/**
* @author Christoph Strobl
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = AppConfig.class)
public class PersonRepositoryTests<K, V> {
/**
* We need to have a Redis server instance available. <br />
* 1) Start/Stop an embedded instance or reuse an already running local installation <br />
* 2) Ignore tests if startup failed and no server running locally.
*/
public static @ClassRule RuleChain rules = RuleChain.outerRule(
EmbeddedRedisServer.runningAt(6379).suppressExceptions()).around(RequiresRedisServer.onLocalhost());
/** {@link Charset} for String conversion **/
private static final Charset CHARSET = Charset.forName("UTF-8");
@Autowired RedisOperations<K, V> ops;
@Autowired PersonRepository repo;
/*
* Set of test users
*/
Person eddard = new Person("eddard", "stark", Gender.MALE);
Person robb = new Person("robb", "stark", Gender.MALE);
Person sansa = new Person("sansa", "stark", Gender.FEMALE);
Person arya = new Person("arya", "stark", Gender.FEMALE);
Person bran = new Person("bran", "stark", Gender.MALE);
Person rickon = new Person("rickon", "stark", Gender.MALE);
Person jon = new Person("jon", "snow", Gender.MALE);
@Before
@After
public void setUp() {
ops.execute((RedisConnection connection) -> {
connection.flushDb();
return "OK";
});
}
/**
* Save a single entity and verify that a key for the given keyspace/prefix exists. <br />
* Print out the hash structure within Redis.
*/
@Test
public void saveSingleEntity() {
repo.save(eddard);
assertThat(ops.execute((RedisConnection connection) -> connection.exists(new String("persons:" + eddard.getId())
.getBytes(CHARSET))), is(true));
}
/**
* Find entity by a single {@link Indexed} property value.
*/
@Test
public void findBySingleProperty() {
flushTestUsers();
List<Person> starks = repo.findByLastname(eddard.getLastname());
assertThat(starks, containsInAnyOrder(eddard, robb, sansa, arya, bran, rickon));
assertThat(starks, not(hasItem(jon)));
}
/**
* Find entities by multiple {@link Indexed} properties using {@literal AND}.
*/
@Test
public void findByMultipleProperties() {
flushTestUsers();
List<Person> aryaStark = repo.findByFirstnameAndLastname(arya.getFirstname(), arya.getLastname());
assertThat(aryaStark, hasItem(arya));
assertThat(aryaStark, not(hasItems(eddard, robb, sansa, bran, rickon, jon)));
}
/**
* Find entities by multiple {@link Indexed} properties using {@literal OR}.
*/
@Test
public void findByMultiplePropertiesUsingOr() {
flushTestUsers();
List<Person> aryaAndJon = repo.findByFirstnameOrLastname(arya.getFirstname(), jon.getLastname());
assertThat(aryaAndJon, containsInAnyOrder(arya, jon));
assertThat(aryaAndJon, not(hasItems(eddard, robb, sansa, bran, rickon)));
}
/**
* Find entities in range defined by {@link Pageable}.
*/
@Test
public void findByReturingPage() {
flushTestUsers();
Page<Person> page1 = repo.findPersonByLastname(eddard.getLastname(), new PageRequest(0, 5));
assertThat(page1.getNumberOfElements(), is(5));
assertThat(page1.getTotalElements(), is(6L));
Page<Person> page2 = repo.findPersonByLastname(eddard.getLastname(), new PageRequest(1, 5));
assertThat(page2.getNumberOfElements(), is(1));
assertThat(page2.getTotalElements(), is(6L));
}
/**
* Find entity by a single {@link Indexed} property on an embedded entity.
*/
@Test
public void findByEmbeddedProperty() {
Address winterfell = new Address();
winterfell.setCountry("the north");
winterfell.setCity("winterfell");
eddard.setAddress(winterfell);
flushTestUsers();
List<Person> eddardStark = repo.findByAddress_City(winterfell.getCity());
assertThat(eddardStark, hasItem(eddard));
assertThat(eddardStark, not(hasItems(robb, sansa, arya, bran, rickon, jon)));
}
/**
* Store references to other entites without embedding all data. <br />
* Print out the hash structure within Redis.
*/
@Test
public void useReferencesToStoreDataToOtherObjects() {
flushTestUsers();
eddard.setChildren(Arrays.asList(jon, robb, sansa, arya, bran, rickon));
repo.save(eddard);
Person laoded = repo.findOne(eddard.getId());
assertThat(laoded.getChildren(), hasItems(jon, robb, sansa, arya, bran, rickon));
/*
* Deceased:
*
* - Robb was killed by Roose Bolton during the Red Wedding.
* - Jon was stabbed by brothers or the Night's Watch.
*/
repo.delete(Arrays.asList(robb, jon));
laoded = repo.findOne(eddard.getId());
assertThat(laoded.getChildren(), hasItems(sansa, arya, bran, rickon));
assertThat(laoded.getChildren(), not(hasItems(robb, jon)));
}
private void flushTestUsers() {
repo.save(Arrays.asList(eddard, robb, sansa, arya, bran, rickon, jon));
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %5p %40.40c:%4L - %m%n</pattern>
</encoder>
</appender>
<root level="warn">
<appender-ref ref="console" />
</root>
</configuration>

View File

@@ -21,6 +21,12 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.github.kstyrc</groupId>
<artifactId>embedded-redis</artifactId>
<version>0.6</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.springdata.redis.test.util;
import java.io.IOException;
import org.junit.rules.ExternalResource;
import redis.embedded.RedisServer;
/**
* JUnit rule implementation to start and shut down an embedded Redis instance.
*
* @author Christoph Strobl
* @author Oliver Gierke
*/
public class EmbeddedRedisServer extends ExternalResource {
private static final int DEFAULT_PORT = 6379;
private RedisServer server;
private int port = DEFAULT_PORT;
private boolean suppressExceptions = false;
public EmbeddedRedisServer() {
}
protected EmbeddedRedisServer(int port) {
this.port = port;
}
public static EmbeddedRedisServer runningAt(Integer port) {
return new EmbeddedRedisServer(port != null ? port : DEFAULT_PORT);
}
public EmbeddedRedisServer suppressExceptions() {
this.suppressExceptions = true;
return this;
}
/*
* (non-Javadoc)
* @see org.junit.rules.ExternalResource#before()
*/
@Override
protected void before() throws IOException {
try {
this.server = new RedisServer(this.port);
this.server.start();
} catch (Exception e) {
if (!suppressExceptions) {
throw e;
}
}
}
/*
* (non-Javadoc)
* @see org.junit.rules.ExternalResource#after()
*/
@Override
protected void after() {
try {
this.server.stop();
} catch (Exception e) {
if (!suppressExceptions) {
throw e;
}
}
}
}