Skip to content

Commit

Permalink
spring-projectsGH-1549: Auto-Detect Type Mappings
Browse files Browse the repository at this point in the history
  • Loading branch information
garyrussell committed Aug 13, 2020
1 parent 82027d3 commit c5aa309
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 0 deletions.
@@ -0,0 +1,48 @@
/*
* Copyright 2020 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
*
* https://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 org.springframework.kafka.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* TODO: Move to spring-messaging?
*
* Classes annotated with this can be automatically populated into various type mappers
* (e.g. Jackson). Several Spring projects, JMS, Kafka, RabbitMQ allow mapping a type id
* to/from a class, for the purpose of decoupling concrete types used by senders and
* receivers.
*
* @author Gary Russell
* @since 2.6
*
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TypeMapping {

/**
* Either a single or multiple type ids to map to/from the annotated class.
* @return the type ids.
*/
String[] value();

}
@@ -0,0 +1,87 @@
/*
* Copyright 2020 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
*
* https://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 org.springframework.kafka.support;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.kafka.annotation.TypeMapping;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

/**
* TODO: Move to spring-messaging?
*
* Utilities for type mappings.
*
* @author Gary Russell
* @since 2.6
*
*/
public final class TypeMappingUtils {

private TypeMappingUtils() {
}

/**
* Find {@link TypeMapping} annotations on classes in the provided packages and return
* as a map of typeid:class.
* @param context the application context.
* @param packagesToScan the packages to scan.
* @return the mappings.
*/
public static Map<String, Class<?>> findTypeMappings(ApplicationContext context, String... packagesToScan) {

Map<String, Class<?>> mappings = new HashMap<>();
Set<Class<?>> candidates = new HashSet<>();
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.setEnvironment(context.getEnvironment());
scanner.setResourceLoader(context);
scanner.addIncludeFilter(new AnnotationTypeFilter(TypeMapping.class));
for (String packageToScan : packagesToScan) {
if (StringUtils.hasText(packageToScan)) {
for (BeanDefinition candidate : scanner.findCandidateComponents(packageToScan)) {
try {
candidates.add(ClassUtils.forName(candidate.getBeanClassName(), context.getClassLoader()));
}
catch (ClassNotFoundException | LinkageError e) {
throw new IllegalStateException(e);
}
}
}
}
for (Class<?> candidate : candidates) {
AnnotationAttributes annotationAttributes =
AnnotatedElementUtils.findMergedAnnotationAttributes(candidate, TypeMapping.class, false, false);
for (String value : annotationAttributes.getStringArray("value")) {
if (StringUtils.hasText(value)) {
mappings.put(value, candidate);
}
}
}
return mappings;
}

}
@@ -0,0 +1,61 @@
/*
* Copyright 2020 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
*
* https://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 org.springframework.kafka.support;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Map;

import org.junit.jupiter.api.Test;

import org.springframework.context.support.GenericApplicationContext;
import org.springframework.kafka.annotation.TypeMapping;

/**
* TODO: Move to spring-messaging?
*
* @author Gary Russell
* @since 2.6
*
*/
public class TypeMappingUtilsTests {

@Test
void testDiscover() {
Map<String, Class<?>> mappings =
TypeMappingUtils.findTypeMappings(new GenericApplicationContext(), getClass().getPackage().getName());
assertThat(mappings).hasSize(6); // this will be 3 if we move this to s-m
assertThat(mappings.get("fooA")).isEqualTo(Foo.class);
assertThat(mappings.get("fooB")).isEqualTo(Foo.class);
assertThat(mappings.get("barA")).isEqualTo(Bar.class);
}

@TypeMapping({ "fooA", "fooB" })
public static class Foo {

public String foo = "foo";

}

@TypeMapping("barA")
public static class Bar extends Foo {

public String bar = "bar";

}

}
Expand Up @@ -30,12 +30,16 @@

import org.apache.kafka.common.errors.SerializationException;
import org.apache.kafka.common.header.Headers;
import org.apache.kafka.common.header.internals.RecordHeader;
import org.apache.kafka.common.header.internals.RecordHeaders;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import org.springframework.context.support.GenericApplicationContext;
import org.springframework.kafka.annotation.TypeMapping;
import org.springframework.kafka.support.TypeMappingUtils;
import org.springframework.kafka.support.converter.AbstractJavaTypeMapper;
import org.springframework.kafka.support.converter.DefaultJackson2JavaTypeMapper;
import org.springframework.kafka.support.converter.Jackson2JavaTypeMapper.TypePrecedence;
Expand Down Expand Up @@ -335,6 +339,26 @@ void testTypeResolverDirect() {
deser.close();
}

@Test
void testDiscoveredTypeMappings() {
DefaultJackson2JavaTypeMapper mapper = new DefaultJackson2JavaTypeMapper();
GenericApplicationContext context = new GenericApplicationContext();
mapper.setTypePrecedence(TypePrecedence.TYPE_ID);
mapper.setIdClassMapping(TypeMappingUtils.findTypeMappings(context, getClass().getPackage().getName()));
JsonDeserializer<Object> deser = new JsonDeserializer<>()
.trustedPackages("*")
.typeMapper(mapper);
Headers headers = new RecordHeaders();
headers.add(new RecordHeader(AbstractJavaTypeMapper.DEFAULT_CLASSID_FIELD_NAME, "foo1".getBytes()));
assertThat(deser.deserialize("", headers, "{\"foo\":\"bar\"}".getBytes())).isInstanceOf(Foo.class);
headers.add(new RecordHeader(AbstractJavaTypeMapper.DEFAULT_CLASSID_FIELD_NAME, "foo2".getBytes()));
assertThat(deser.deserialize("", headers, "{\"foo\":\"bar\"}".getBytes())).isInstanceOf(Foo.class);
headers.add(new RecordHeader(AbstractJavaTypeMapper.DEFAULT_CLASSID_FIELD_NAME, "bar".getBytes()));
assertThat(deser.deserialize("", headers, "{\"bar\":\"baz\"}".getBytes()))
.isInstanceOf(Bar.class);
deser.close();
}

public static JavaType fooBarJavaType(byte[] data, Headers headers) {
if (data[0] == '{' && data[1] == 'f') {
return TypeFactory.defaultInstance().constructType(Foo.class);
Expand Down Expand Up @@ -369,12 +393,14 @@ static class DummyEntityArrayJsonDeserializer extends JsonDeserializer<DummyEnti

}

@TypeMapping({ "foo1", "foo2" })
public static class Foo {

public String foo = "foo";

}

@TypeMapping("bar")
public static class Bar extends Foo {

public String bar = "bar";
Expand Down

0 comments on commit c5aa309

Please sign in to comment.