Skip to content

Commit 92274a8

Browse files
mp911dechristophstrobl
authored andcommitted
DATAREDIS-425 - Add JSR-310 support, CDI extension and Update reference documentation.
We ship converters for JSR-310 types (LocalDate/Time, ZonedDateTime, Period, Duration and ZoneId) to map between UTF-8-encoded byte[] and JDK 8 date/time types. We also export Redis Repositories in a CDI environment. Repositories can be injected using @Inject. The CDI extension requires at least RedisOperations to be provided. Other beans like RedisKeyValueAdapter and RedisKeyValueTemplate can be provided by the user. If no RedisKeyValueAdapter/RedisKeyValueTemplate beans are found, the CDI extension creates own managed instances. Original Pull Request: #156
1 parent 4362c58 commit 92274a8

35 files changed

+1972
-61
lines changed

.travis.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
language: java
22
jdk:
3-
- oraclejdk7
43
- oraclejdk8
54
env:
65
matrix:

pom.xml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,36 @@
152152
<optional>true</optional>
153153
</dependency>
154154

155+
<!-- CDI -->
156+
<dependency>
157+
<groupId>javax.enterprise</groupId>
158+
<artifactId>cdi-api</artifactId>
159+
<version>${cdi}</version>
160+
<scope>provided</scope>
161+
<optional>true</optional>
162+
</dependency>
163+
164+
<dependency>
165+
<groupId>javax.el</groupId>
166+
<artifactId>el-api</artifactId>
167+
<version>${cdi}</version>
168+
<scope>test</scope>
169+
</dependency>
170+
171+
<dependency>
172+
<groupId>org.apache.openwebbeans.test</groupId>
173+
<artifactId>cditest-owb</artifactId>
174+
<version>${webbeans}</version>
175+
<scope>test</scope>
176+
</dependency>
177+
178+
<dependency>
179+
<groupId>javax.servlet</groupId>
180+
<artifactId>servlet-api</artifactId>
181+
<version>2.5</version>
182+
<scope>test</scope>
183+
</dependency>
184+
155185
<!-- Test -->
156186

157187
<dependency>

src/main/asciidoc/reference/redis-repositories.adoc

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ To access domain entities stored in a Redis you can leverage repository support
1414
====
1515
[source,java]
1616
----
17-
@RedisHash("persons");
17+
@RedisHash("persons")
1818
public class Person {
1919
2020
@Id String id;
@@ -158,7 +158,7 @@ of Complex Type
158158
addresses.[work].city = "...
159159
|===
160160

161-
Mapping behavior can be customized by registering the according `Converter` in `CustomConversions`. Those converters can take care of converting from/to a single `byte[]` as well as `Map<String,byte[]>` whereas the first one is suitable for eg. converting one complex type to eg. a binary JSON representation that still uses the default mappings hash structure, whereas the second options offers full control over the resulting hash.
161+
Mapping behavior can be customized by registering the according `Converter` in `CustomConversions`. Those converters can take care of converting from/to a single `byte[]` as well as `Map<String,byte[]>` whereas the first one is suitable for eg. converting one complex type to eg. a binary JSON representation that still uses the default mappings hash structure. The second option offers full control over the resulting hash. Writing objects to a Redis hash will delete the content from the hash and re-create the whole hash, so not mapped data will be lost.
162162

163163
.Sample byte[] Converters
164164
====
@@ -308,15 +308,15 @@ public class ApplicationConfig {
308308

309309
[[redis.repositories.indexes]]
310310
== Secondary Indexes
311-
http://redis.io/topics/indexes[Secondary indexes] are used to enable lookup operations based on native redis structures. Values are written to the according indexes on every save and are removed when objects are deleted or <<redis.repositories.expirations,expire>>.
311+
http://redis.io/topics/indexes[Secondary indexes] are used to enable lookup operations based on native Redis structures. Values are written to the according indexes on every save and are removed when objects are deleted or <<redis.repositories.expirations,expire>>.
312312

313313
Given the sample `Person` entity we can create an index for _firstname_ by annotating the property with `@Indexed`.
314314

315315
.Annotation driven indexing
316316
====
317317
[source,java]
318318
----
319-
@RedisHash("persons");
319+
@RedisHash("persons")
320320
public class Person {
321321
322322
@Id String id;
@@ -351,7 +351,7 @@ Further more the programmatic setup allows to define indexes on map keys and lis
351351
====
352352
[source,java]
353353
----
354-
@RedisHash("persons");
354+
@RedisHash("persons")
355355
public class Person {
356356
357357
// ... other properties omitted
@@ -457,16 +457,19 @@ public class TimeToLiveOnMethod {
457457
The repository implementation ensures subscription to http://redis.io/topics/notifications[Redis keyspace notifications] via `RedisMessageListenerContainer`.
458458

459459
When the expiration is set to a positive value the according `EXPIRE` command is executed.
460-
Additionally to persisting the original, a _phantom_ copy is persisted in Redis and set to expire 5 minutes after the original one. This done to enable the Repository support to publish `RedisKeyExpiredEvent` holding the expired value via Springs `ApplicationEventPublisher` whenever a key expires even though the original values have already been gone.
460+
Additionally to persisting the original, a _phantom_ copy is persisted in Redis and set to expire 5 minutes after the original one. This is done to enable the Repository support to publish `RedisKeyExpiredEvent` holding the expired value via Springs `ApplicationEventPublisher` whenever a key expires even though the original values have already been gone. Expiry events
461+
will be received on all connected applications using Spring Data Redis repositories.
461462

462463
The `RedisKeyExpiredEvent` will hold a copy of the actually expired domain object as well as the key.
463464

464-
NOTE: The keyspace notification message listener will alter `notify-keyspace-events` settings in Redis if those are not already set. Existing settings will not be overridden so it is left to the user to set those up correctly when not leaving them empty.
465+
NOTE: The keyspace notification message listener will alter `notify-keyspace-events` settings in Redis if those are not already set. Existing settings will not be overridden, so it is left to the user to set those up correctly when not leaving them empty.
466+
467+
NOTE: Redis Pub/Sub messages are not persistent. If a key expires while the application is down the expiry event will not be processed which may lead to secondary indexes containing still references to the expired object.
465468

466469
[[redis.repositories.references]]
467470
== Persisting References
468-
Marking properties with `@Reference` allows to store a simple key reference instead of copying the all values into the hash itself.
469-
On loading from Redis references are resolved automatically and mapped back into the object.
471+
Marking properties with `@Reference` allows storing a simple key reference instead of copying values into the hash itself.
472+
On loading from Redis, references are resolved automatically and mapped back into the object.
470473

471474
.Sample Property Reference
472475
====
@@ -499,9 +502,12 @@ public interface PersonRepository extends CrudRepository<Person, String> {
499502
----
500503
====
501504

505+
502506
NOTE: Please make sure properties used in finder methods are set up for indexing.
503507

504-
Using derived finder methods might not always be sufficient to model the queries to execute. `RedisCallback` offers more control over the actual matching of index structures or even customly added ones. All it takes is providing a `RedisCallback` that returns a single or `Iterable` set of _id_ values.
508+
NOTE: Query methods for Redis repositories support only queries for entities and collections of entities with paging.
509+
510+
Using derived query methods might not always be sufficient to model the queries to execute. `RedisCallback` offers more control over the actual matching of index structures or even custom added ones. All it takes is providing a `RedisCallback` that returns a single or `Iterable` set of _id_ values.
505511

506512
.Sample finder using RedisCallback
507513
====
@@ -518,6 +524,81 @@ List<RedisSession> sessionsByUser = template.find(new RedisCallback<Set<byte[]>>
518524
----
519525
====
520526

527+
Here's an overview of the keywords supported for Redis and what a method containing that keyword essentially translates to.
528+
====
529+
530+
.Supported keywords inside method names
531+
[options = "header, autowidth"]
532+
|===============
533+
|Keyword|Sample|Redis snippet
534+
|`And`|`findByLastnameAndFirstname`|`SINTER …:firstname:rand …:lastname:al’thor`
535+
|`Or`|`findByLastnameOrFirstname`|`SUNION …:firstname:rand …:lastname:al’thor`
536+
|`Is,Equals`|`findByFirstname`,`findByFirstnameIs`,`findByFirstnameEquals`|`SINTER …:firstname:rand`
537+
|===============
538+
====
539+
540+
[[redis.misc.cdi-integration]]
541+
== CDI integration
542+
543+
Instances of the repository interfaces are usually created by a container, which Spring is the most natural choice when working with Spring Data. There's sophisticated support to easily set up Spring to create bean instances. Spring Data Redis ships with a custom CDI extension that allows using the repository abstraction in CDI environments. The extension is part of the JAR so all you need to do to activate it is dropping the Spring Data Redis JAR into your classpath.
544+
545+
You can now set up the infrastructure by implementing a CDI Producer for the `RedisConnectionFactory` and `RedisOperations`:
546+
547+
[source, java]
548+
----
549+
class RedisOperationsProducer {
550+
551+
552+
@Produces
553+
RedisConnectionFactory redisConnectionFactory() {
554+
555+
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
556+
jedisConnectionFactory.setHostName("localhost");
557+
jedisConnectionFactory.setPort(6379);
558+
jedisConnectionFactory.afterPropertiesSet();
559+
560+
return jedisConnectionFactory;
561+
}
562+
563+
void disposeRedisConnectionFactory(@Disposes RedisConnectionFactory redisConnectionFactory) throws Exception {
564+
565+
if (redisConnectionFactory instanceof DisposableBean) {
566+
((DisposableBean) redisConnectionFactory).destroy();
567+
}
568+
}
569+
570+
@Produces
571+
@ApplicationScoped
572+
RedisOperations<byte[], byte[]> redisOperationsProducer(RedisConnectionFactory redisConnectionFactory) {
573+
574+
RedisTemplate<byte[], byte[]> template = new RedisTemplate<byte[], byte[]>();
575+
template.setConnectionFactory(redisConnectionFactory);
576+
template.afterPropertiesSet();
577+
578+
return template;
579+
}
580+
581+
}
582+
----
583+
584+
The necessary setup can vary depending on the JavaEE environment you run in.
585+
586+
The Spring Data Redis CDI extension will pick up all Repositories available as CDI beans and create a proxy for a Spring Data repository whenever a bean of a repository type is requested by the container. Thus obtaining an instance of a Spring Data repository is a matter of declaring an `@Injected` property:
587+
588+
[source, java]
589+
----
590+
class RepositoryClient {
591+
592+
@Inject
593+
PersonRepository repository;
594+
595+
public void businessMethod() {
596+
List<Person> people = repository.findAll();
597+
}
598+
}
599+
----
600+
601+
A Redis Repository requires `RedisKeyValueAdapter` and `RedisKeyValueTemplate` instances. These beans are created and managed by the Spring Data CDI extension if no provided beans are found. You can however supply your own beans to configure the specific properties of `RedisKeyValueAdapter` and `RedisKeyValueTemplate`.
521602

522603

523604

src/main/java/org/springframework/data/redis/core/RedisKeyExpiredEvent.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
*/
3030
public class RedisKeyExpiredEvent<T> extends RedisKeyspaceEvent {
3131

32+
/**
33+
* Use {@literal UTF-8} as default charset.
34+
*/
35+
public static final Charset CHARSET = Charset.forName("UTF-8");
36+
3237
private final byte[][] args;
3338
private final Object value;
3439

@@ -62,7 +67,7 @@ public RedisKeyExpiredEvent(byte[] key, Object value) {
6267
public String getKeyspace() {
6368

6469
if (args.length >= 2) {
65-
return new String(args[0], Charset.forName("UTF-8"));
70+
return new String(args[0], CHARSET);
6671
}
6772

6873
return null;

src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ public RedisKeyValueAdapter(RedisOperations<?, ?> redisOps, RedisConverter redis
160160
initKeyExpirationListener();
161161
}
162162

163+
/**
164+
* Default constructor.
165+
*/
166+
protected RedisKeyValueAdapter() {
167+
}
168+
163169
/*
164170
* (non-Javadoc)
165171
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#put(java.io.Serializable, java.lang.Object, java.io.Serializable)

src/main/java/org/springframework/data/redis/core/RedisQueryEngine.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public RedisQueryEngine(CriteriaAccessor<RedisOperationChain> criteriaAccessor,
6666
* (non-Javadoc)
6767
* @see org.springframework.data.keyvalue.core.QueryEngine#execute(java.lang.Object, java.lang.Object, int, int, java.io.Serializable, java.lang.Class)
6868
*/
69+
@Override
6970
public <T> Collection<T> execute(final RedisOperationChain criteria, final Comparator<?> sort, final int offset,
7071
final int rows, final Serializable keyspace, Class<T> type) {
7172

src/main/java/org/springframework/data/redis/core/convert/BinaryConverters.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ private BinaryConverters() {}
4545
* @author Christoph Strobl
4646
* @since 1.7
4747
*/
48-
private static class StringBasedConverter {
48+
static class StringBasedConverter {
4949

5050
byte[] fromString(String source) {
5151

@@ -166,7 +166,7 @@ public T convert(byte[] source) {
166166
if (value == null || value.length() == 0) {
167167
return null;
168168
}
169-
return (T) Enum.valueOf(this.enumType, value.trim());
169+
return Enum.valueOf(this.enumType, value.trim());
170170
}
171171
}
172172
}
@@ -183,8 +183,8 @@ public <T extends Number> Converter<byte[], T> getConverter(Class<T> targetType)
183183
return new BytesToNumberConverter<T>(targetType);
184184
}
185185

186-
private static final class BytesToNumberConverter<T extends Number> extends StringBasedConverter implements
187-
Converter<byte[], T> {
186+
private static final class BytesToNumberConverter<T extends Number> extends StringBasedConverter
187+
implements Converter<byte[], T> {
188188

189189
private final Class<T> targetType;
190190

src/main/java/org/springframework/data/redis/core/convert/Bucket.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ public boolean isEmpty() {
9898
return data.isEmpty();
9999
}
100100

101+
/**
102+
* @return the number of key-value mappings of the {@link Bucket}.
103+
*/
104+
public int size() {
105+
return data.size();
106+
}
107+
101108
/**
102109
* @return never {@literal null}.
103110
*/
@@ -121,6 +128,12 @@ public Map<String, byte[]> asMap() {
121128
return Collections.unmodifiableMap(this.data);
122129
}
123130

131+
/**
132+
* Extracts a bucket containing key/value pairs with the {@code prefix}.
133+
*
134+
* @param prefix
135+
* @return
136+
*/
124137
public Bucket extract(String prefix) {
125138

126139
Bucket partial = new Bucket();

src/main/java/org/springframework/data/redis/core/convert/CustomConversions.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ public CustomConversions(List<?> converters) {
103103
toRegister.add(new BinaryConverters.DateToBytesConverter());
104104
toRegister.add(new BinaryConverters.BytesToDateConverter());
105105

106+
toRegister.addAll(Jsr310Converters.getConvertersToRegister());
107+
106108
for (Object c : toRegister) {
107109
registerConversion(c);
108110
}
@@ -161,7 +163,8 @@ public void registerConvertersIn(GenericConversionService conversionService) {
161163
}
162164

163165
if (!added) {
164-
throw new IllegalArgumentException("Given set contains element that is neither Converter nor ConverterFactory!");
166+
throw new IllegalArgumentException(
167+
"Given set contains element that is neither Converter nor ConverterFactory!");
165168
}
166169
}
167170
}

0 commit comments

Comments
 (0)