diff --git a/spring-core/src/main/java/org/springframework/core/hint/AbstractTypeReference.java b/spring-core/src/main/java/org/springframework/core/hint/AbstractTypeReference.java new file mode 100644 index 0000000000..bbe77ec818 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/AbstractTypeReference.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.util.Objects; + +/** + * Base {@link TypeReference} implementation that ensures consistent behaviour + * for {@code equals()}, {@code hashCode()}, and {@code toString()} based on + * the {@linkplain #getCanonicalName() canonical name}. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public abstract class AbstractTypeReference implements TypeReference { + + @Override + public int hashCode() { + return Objects.hash(getCanonicalName()); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof TypeReference otherReference)) { + return false; + } + return getCanonicalName().equals(otherReference.getCanonicalName()); + } + + @Override + public String toString() { + return getCanonicalName(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/ClassProxyHint.java b/spring-core/src/main/java/org/springframework/core/hint/ClassProxyHint.java new file mode 100644 index 0000000000..f7862fef21 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/ClassProxyHint.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * A hint that describes the need for a proxy against a concrete class. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public final class ClassProxyHint { + + private final TypeReference targetClass; + + private final List proxiedInterfaces; + + + private ClassProxyHint(Builder builder) { + this.targetClass = builder.targetClass; + this.proxiedInterfaces = builder.proxiedInterfaces.stream().distinct().toList(); + } + + /** + * Initialize a builder with the target class to use. + * @param targetClass the target class of the proxy + * @return a builder for the hint + */ + public static Builder of(TypeReference targetClass) { + return new Builder(targetClass); + } + + /** + * Initialize a builder with the target class to use. + * @param targetClass the target class of the proxy + * @return a builder for the hint + */ + public static Builder of(Class targetClass) { + return of(TypeReference.of(targetClass)); + } + + /** + * Return the target class of the proxy. + * @return the target class + */ + public TypeReference getTargetClass() { + return this.targetClass; + } + + /** + * Return the interfaces to be proxied. + * @return the interfaces that the proxy should implement + */ + public List getProxiedInterfaces() { + return this.proxiedInterfaces; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ClassProxyHint that = (ClassProxyHint) o; + return this.targetClass.equals(that.targetClass) + && this.proxiedInterfaces.equals(that.proxiedInterfaces); + } + + @Override + public int hashCode() { + return Objects.hash(this.targetClass, this.proxiedInterfaces); + } + + + /** + * Builder for {@link ClassProxyHint}. + */ + public static class Builder { + + private final TypeReference targetClass; + + private final LinkedList proxiedInterfaces = new LinkedList<>(); + + + public Builder(TypeReference targetClass) { + this.targetClass = targetClass; + } + + /** + * Add the specified interfaces that the proxy should implement. + * @param proxiedInterfaces the interfaces the proxy should implement + * @return {@code this}, to facilitate method chaining + */ + public Builder proxiedInterfaces(TypeReference... proxiedInterfaces) { + this.proxiedInterfaces.addAll(Arrays.asList(proxiedInterfaces)); + return this; + } + + /** + * Add the specified interfaces that the proxy should implement. + * @param proxiedInterfaces the interfaces the proxy should implement + * @return {@code this}, to facilitate method chaining + */ + public Builder proxiedInterfaces(Class... proxiedInterfaces) { + this.proxiedInterfaces.addAll(Arrays.stream(proxiedInterfaces) + .map(TypeReference::of).collect(Collectors.toList())); + return this; + } + + /** + * Create a {@link ClassProxyHint} based on the state of this builder. + * @return a class proxy hint + */ + public ClassProxyHint build() { + return new ClassProxyHint(this); + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/ExecutableHint.java b/spring-core/src/main/java/org/springframework/core/hint/ExecutableHint.java new file mode 100644 index 0000000000..c45dae5598 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/ExecutableHint.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.util.ObjectUtils; + +/** + * A hint that describes the need for reflection on a {@link Method} or + * {@link Constructor}. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public final class ExecutableHint extends MemberHint { + + private final List parameterTypes; + + private final List modes; + + + private ExecutableHint(Builder builder) { + super(builder.name); + this.parameterTypes = List.copyOf(builder.parameterTypes); + this.modes = List.copyOf(builder.modes); + } + + /** + * Initialize a builder with the parameter types of a constructor. + * @param parameterTypes the parameter types of the constructor + * @return a builder + */ + public static Builder ofConstructor(List parameterTypes) { + return new Builder("", parameterTypes); + } + + /** + * Initialize a builder with the name and parameters types of a method. + * @param name the name of the method + * @param parameterTypes the parameter types of the method + * @return a builder + */ + public static Builder ofMethod(String name, List parameterTypes) { + return new Builder(name, parameterTypes); + } + + /** + * Return the parameter types of the executable. + * @return the parameter types + * @see Executable#getParameterTypes() + */ + public List getParameterTypes() { + return this.parameterTypes; + } + + /** + * Return the {@linkplain ExecutableMode modes} that apply to this hint. + * @return the modes + */ + public List getModes() { + return this.modes; + } + + + /** + * Builder for {@link ExecutableHint}. + */ + public static final class Builder { + + private final String name; + + private final List parameterTypes; + + private final Set modes = new LinkedHashSet<>(); + + + private Builder(String name, List parameterTypes) { + this.name = name; + this.parameterTypes = parameterTypes; + } + + /** + * Add the specified {@linkplain ExecutableMode mode} if necessary. + * @param mode the mode to add + * @return {@code this}, to facilitate method chaining + */ + public Builder withMode(ExecutableMode mode) { + this.modes.add(mode); + return this; + } + + /** + * Set the {@linkplain ExecutableMode modes} to use. + * @param modes the mode to use + * @return {@code this}, to facilitate method chaining + */ + public Builder setModes(ExecutableMode... modes) { + this.modes.clear(); + if (!ObjectUtils.isEmpty(modes)) { + this.modes.addAll(Arrays.asList(modes)); + } + return this; + } + + /** + * Create an {@link ExecutableHint} based on the state of this builder. + * @return an executable hint + */ + public ExecutableHint build() { + return new ExecutableHint(this); + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/ExecutableMode.java b/spring-core/src/main/java/org/springframework/core/hint/ExecutableMode.java new file mode 100644 index 0000000000..c562eef6b2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/ExecutableMode.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.lang.reflect.Executable; + +/** + * Represent the need of reflection for a given {@link Executable}. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public enum ExecutableMode { + + /** + * Only retrieving the {@link Executable} and its metadata is required. + */ + INTROSPECT, + + /** + * Full reflection support is required, including the ability to invoke + * the {@link Executable}. + */ + INVOKE; + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/FieldHint.java b/spring-core/src/main/java/org/springframework/core/hint/FieldHint.java new file mode 100644 index 0000000000..f40ae98437 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/FieldHint.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.lang.reflect.Field; + +/** + * A hint that describes the need of reflection on a {@link Field}. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public final class FieldHint extends MemberHint { + + private final boolean allowWrite; + + private final boolean allowUnsafeAccess; + + + private FieldHint(Builder builder) { + super(builder.name); + this.allowWrite = builder.allowWrite; + this.allowUnsafeAccess = builder.allowUnsafeAccess; + } + + /** + * Return whether setting the value of the field should be allowed. + * @return {@code true} to allow {@link Field#set(Object, Object)}. + */ + public boolean isAllowWrite() { + return this.allowWrite; + } + + /** + * Return whether if using {@code Unsafe} on the field should be allowed. + * @return {@code true} to allow unsafe access + */ + public boolean isAllowUnsafeAccess() { + return this.allowUnsafeAccess; + } + + + /** + * Builder for {@link FieldHint}. + */ + public static class Builder { + + private final String name; + + private boolean allowWrite; + + private boolean allowUnsafeAccess; + + + public Builder(String name) { + this.name = name; + } + + /** + * Specify if setting the value of the field should be allowed. + * @param allowWrite {@code true} to allow {@link Field#set(Object, Object)} + * @return {@code this}, to facilitate method chaining + */ + public Builder allowWrite(boolean allowWrite) { + this.allowWrite = allowWrite; + return this; + } + + /** + * Specify if using {@code Unsafe} on the field should be allowed. + * @param allowUnsafeAccess {@code true} to allow unsafe access + * @return {@code this}, to facilitate method chaining + */ + public Builder allowUnsafeAccess(boolean allowUnsafeAccess) { + this.allowUnsafeAccess = allowUnsafeAccess; + return this; + } + + /** + * Create a {@link FieldHint} based on the state of this builder. + * @return a field hint + */ + public FieldHint build() { + return new FieldHint(this); + } + + } +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/JavaSerializationHints.java b/spring-core/src/main/java/org/springframework/core/hint/JavaSerializationHints.java new file mode 100644 index 0000000000..b8d06a1ca2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/JavaSerializationHints.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.io.Serializable; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Stream; + +/** + * Gather the need for Java serialization at runtime. + * + * @author Stephane Nicoll + * @since 6.0 + * @see Serializable + */ +public class JavaSerializationHints { + + private final Set types; + + + public JavaSerializationHints() { + this.types = new LinkedHashSet<>(); + } + + /** + * Return the {@link TypeReference types} that need to be serialized using + * Java serialization at runtime. + * @return a stream of {@link Serializable} types + */ + public Stream types() { + return this.types.stream(); + } + + /** + * Register that the type defined by the specified {@link TypeReference} + * need to be serialized using java serialization. + * @param type the type to register + * @return {@code this}, to facilitate method chaining + */ + public JavaSerializationHints registerType(TypeReference type) { + this.types.add(type); + return this; + } + + /** + * Register that the specified type need to be serialized using java + * serialization. + * @param type the type to register + * @return {@code this}, to facilitate method chaining + */ + public JavaSerializationHints registerType(Class type) { + return registerType(TypeReference.of(type)); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/JdkProxyHint.java b/spring-core/src/main/java/org/springframework/core/hint/JdkProxyHint.java new file mode 100644 index 0000000000..4429ac9f7d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/JdkProxyHint.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * A hint that describes the need of a JDK {@link Proxy}, that is an + * interfaces-based proxy. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public final class JdkProxyHint { + + private final List proxiedInterfaces; + + + private JdkProxyHint(Builder builder) { + this.proxiedInterfaces = List.copyOf(builder.proxiedInterfaces); + } + + /** + * Return the interfaces to be proxied. + * @return the interfaces that the proxy should implement + */ + public List getProxiedInterfaces() { + return this.proxiedInterfaces; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JdkProxyHint that = (JdkProxyHint) o; + return this.proxiedInterfaces.equals(that.proxiedInterfaces); + } + + @Override + public int hashCode() { + return Objects.hash(this.proxiedInterfaces); + } + + + /** + * Builder for {@link JdkProxyHint}. + */ + public static class Builder { + + private final LinkedList proxiedInterfaces = new LinkedList<>(); + + + /** + * Add the specified interfaces that the proxy should implement. + * @param proxiedInterfaces the interfaces the proxy should implement + * @return {@code this}, to facilitate method chaining + */ + public Builder proxiedInterfaces(TypeReference... proxiedInterfaces) { + this.proxiedInterfaces.addAll(Arrays.asList(proxiedInterfaces)); + return this; + } + + /** + * Add the specified interfaces that the proxy should implement. + * @param proxiedInterfaces the interfaces the proxy should implement + * @return {@code this}, to facilitate method chaining + */ + public Builder proxiedInterfaces(Class... proxiedInterfaces) { + this.proxiedInterfaces.addAll(Arrays.stream(proxiedInterfaces) + .map(TypeReference::of).collect(Collectors.toList())); + return this; + } + + /** + * Create a {@link JdkProxyHint} based on the state of this builder. + * @return a jdk proxy hint + */ + public JdkProxyHint build() { + return new JdkProxyHint(this); + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/MemberCategory.java b/spring-core/src/main/java/org/springframework/core/hint/MemberCategory.java new file mode 100644 index 0000000000..6cf0b44fd8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/MemberCategory.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; + +/** + * Represent predefined {@linkplain Member members} groups. + * + * @author Andy Clement + * @author Sebastien Deleuze + * @author Stephane Nicoll + * @since 6.0 + */ +public enum MemberCategory { + + /** + * A category that represents public {@linkplain Field fields}. + * @see Class#getFields() + */ + PUBLIC_FIELDS, + + /** + * A category that represents {@linkplain Class#getDeclaredFields() declared + * fields}, that is all fields defined by the class, but not inherited ones. + * @see Class#getDeclaredFields() + */ + DECLARED_FIELDS, + + /** + * A category that defines public {@linkplain Constructor constructors} can + * be introspected, but not invoked. + * @see Class#getConstructors() + * @see ExecutableMode#INTROSPECT + */ + INTROSPECT_PUBLIC_CONSTRUCTORS, + + /** + * A category that defines {@linkplain Class#getDeclaredConstructors() all + * constructors} can be introspected, but not invoked. + * @see Class#getDeclaredConstructors() + * @see ExecutableMode#INTROSPECT + */ + INTROSPECT_DECLARED_CONSTRUCTORS, + + /** + * A category that defines public {@linkplain Constructor constructors} can + * be invoked. + * @see Class#getConstructors() + * @see ExecutableMode#INVOKE + */ + INVOKE_PUBLIC_CONSTRUCTORS, + + /** + * A category that defines {@linkplain Class#getDeclaredConstructors() all + * constructors} can be invoked. + * @see Class#getDeclaredConstructors() + * @see ExecutableMode#INVOKE + */ + INVOKE_DECLARED_CONSTRUCTORS, + + /** + * A category that defines public {@linkplain Method methods}, including + * inherited ones can be introspect, but not invoked. + * @see Class#getMethods() + * @see ExecutableMode#INTROSPECT + */ + INTROSPECT_PUBLIC_METHODS, + + /** + * A category that defines {@linkplain Class#getDeclaredMethods() all + * methods}, excluding inherited ones can be introspected, but not invoked. + * @see Class#getDeclaredMethods() + * @see ExecutableMode#INTROSPECT + */ + INTROSPECT_DECLARED_METHODS, + + /** + * A category that defines public {@linkplain Method methods}, including + * inherited ones can be invoked. + * @see Class#getMethods() + * @see ExecutableMode#INVOKE + */ + INVOKE_PUBLIC_METHODS, + + /** + * A category that defines {@linkplain Class#getDeclaredMethods() all + * methods}, excluding inherited ones can be invoked. + * @see Class#getDeclaredMethods() + * @see ExecutableMode#INVOKE + */ + INVOKE_DECLARED_METHODS, + + /** + * A category that represents public {@linkplain Class#getClasses() inner + * classes}. Contrary to other categories, this does not register any + * particular reflection for them but rather make sure they are available + * via a call to {@link Class#getClasses}. + */ + PUBLIC_CLASSES, + + /** + * A category that represents all {@linkplain Class#getDeclaredClasses() + * inner classes}. Contrary to other categories, this does not register any + * particular reflection for them but rather make sure they are available + * via a call to {@link Class#getDeclaredClasses}. + */ + DECLARED_CLASSES; + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/MemberHint.java b/spring-core/src/main/java/org/springframework/core/hint/MemberHint.java new file mode 100644 index 0000000000..16edee6648 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/MemberHint.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.lang.reflect.Member; + +/** + * Base hint that describes the need for reflection on a {@link Member}. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public abstract class MemberHint { + + private final String name; + + + protected MemberHint(String name) { + this.name = name; + } + + /** + * Return the name of the member. + * @return the name + */ + public String getName() { + return this.name; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/ProxyHints.java b/spring-core/src/main/java/org/springframework/core/hint/ProxyHints.java new file mode 100644 index 0000000000..e542212d88 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/ProxyHints.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.core.hint.ClassProxyHint.Builder; + +/** + * Gather the need of using proxies at runtime. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public class ProxyHints { + + private final Set jdkProxies = new LinkedHashSet<>(); + + private final Set classProxies = new LinkedHashSet<>(); + + + /** + * Return the interfaces-based proxies that are required. + * @return a stream of {@link JdkProxyHint} + */ + public Stream jdkProxies() { + return this.jdkProxies.stream(); + } + + /** + * Return the class-based proxies that are required. + * @return a stream of {@link ClassProxyHint} + */ + public Stream classProxies() { + return this.classProxies.stream(); + } + + /** + * Register a {@link JdkProxyHint}. + * @param hint the supplier to the hint + * @return {@code this}, to facilitate method chaining + */ + public ProxyHints registerJdkProxy(Supplier hint) { + this.jdkProxies.add(hint.get()); + return this; + } + + /** + * Register that a JDK proxy implementing the interfaces defined by the + * specified {@link TypeReference type references} is required. + * @param proxiedInterfaces the interfaces the proxy should implement + * @return {@code this}, to facilitate method chaining + */ + public ProxyHints registerJdkProxy(TypeReference... proxiedInterfaces) { + return registerJdkProxy(() -> new JdkProxyHint.Builder() + .proxiedInterfaces(proxiedInterfaces).build()); + } + + /** + * Register that a JDK proxy implementing the specified interfaces is + * required. + * @param proxiedInterfaces the interfaces the proxy should implement + * @return {@code this}, to facilitate method chaining + */ + public ProxyHints registerJdkProxy(Class... proxiedInterfaces) { + List concreteTypes = Arrays.stream(proxiedInterfaces) + .filter(candidate -> !candidate.isInterface()).map(Class::getName).collect(Collectors.toList()); + if (!concreteTypes.isEmpty()) { + throw new IllegalArgumentException("Not an interface: " + concreteTypes); + } + return registerJdkProxy(() -> new JdkProxyHint.Builder() + .proxiedInterfaces(proxiedInterfaces).build()); + } + + /** + * Register that a class proxy is required for the class defined by the + * specified {@link TypeReference}. + * @param targetClass the target class of the proxy + * @param classProxyHint a builder to further customize the hint for that proxy + * @return {@code this}, to facilitate method chaining + */ + public ProxyHints registerClassProxy(TypeReference targetClass, Consumer classProxyHint) { + Builder builder = ClassProxyHint.of(targetClass); + classProxyHint.accept(builder); + this.classProxies.add(builder.build()); + return this; + } + + /** + * Register that a class proxy is required for the specified class. + * @param targetClass the target class of the proxy + * @param classProxyHint a builder to further customize the hint for that proxy + * @return {@code this}, to facilitate method chaining + */ + public ProxyHints registerClassProxy(Class targetClass, Consumer classProxyHint) { + if (targetClass.isInterface()) { + throw new IllegalArgumentException("Should not be an interface: " + targetClass); + } + return registerClassProxy(TypeReference.of(targetClass), classProxyHint); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/ReflectionHints.java b/spring-core/src/main/java/org/springframework/core/hint/ReflectionHints.java new file mode 100644 index 0000000000..1a8493e7e8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/ReflectionHints.java @@ -0,0 +1,145 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.core.hint.TypeHint.Builder; + +/** + * Gather the need for reflection at runtime. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public class ReflectionHints { + + private final Map types = new HashMap<>(); + + + /** + * Return the types that require reflection. + * @return the type hints + */ + public Stream typeHints() { + return this.types.values().stream().map(TypeHint.Builder::build); + } + + /** + * Register or customize reflection hints for the type defined by the + * specified {@link TypeReference}. + * @param type the type to customize + * @param typeHint a builder to further customize hints for that type + * @return {@code this}, to facilitate method chaining + */ + public ReflectionHints registerType(TypeReference type, Consumer typeHint) { + Builder builder = this.types.computeIfAbsent(type, TypeHint.Builder::new); + typeHint.accept(builder); + return this; + } + + /** + * Register or customize reflection hints for the specified type. + * @param type the type to customize + * @param typeHint a builder to further customize hints for that type + * @return {@code this}, to facilitate method chaining + */ + public ReflectionHints registerType(Class type, Consumer typeHint) { + return registerType(TypeReference.of(type), typeHint); + } + + /** + * Register the need for reflection on the specified {@link Field}. + * @param field the field that requires reflection + * @param fieldHint a builder to further customize the hints of this field + * @return {@code this}, to facilitate method chaining + */ + public ReflectionHints registerField(Field field, Consumer fieldHint) { + return registerType(TypeReference.of(field.getDeclaringClass()), + typeHint -> typeHint.withField(field.getName(), fieldHint)); + } + + /** + * Register the need for reflection on the specified {@link Field}, + * enabling write access. + * @param field the field that requires reflection + * @return {@code this}, to facilitate method chaining + */ + public ReflectionHints registerField(Field field) { + return registerField(field, fieldHint -> fieldHint.allowWrite(true)); + } + + /** + * Register the need for reflection on the specified {@link Constructor}. + * @param constructor the constructor that requires reflection + * @param constructorHint a builder to further customize the hints of this + * constructor + * @return {@code this}, to facilitate method chaining + */ + public ReflectionHints registerConstructor(Constructor constructor, Consumer constructorHint) { + return registerType(TypeReference.of(constructor.getDeclaringClass()), + typeHint -> typeHint.withConstructor(mapParameters(constructor), constructorHint)); + } + + /** + * Register the need for reflection on the specified {@link Constructor}, + * enabling {@link ExecutableMode#INVOKE}. + * @param constructor the constructor that requires reflection + * @return {@code this}, to facilitate method chaining + */ + public ReflectionHints registerConstructor(Constructor constructor) { + return registerConstructor(constructor, constructorHint -> + constructorHint.withMode(ExecutableMode.INVOKE)); + } + + /** + * Register the need for reflection on the specified {@link Method}. + * @param method the method that requires reflection + * @param methodHint a builder to further customize the hints of this method + * @return {@code this}, to facilitate method chaining + */ + public ReflectionHints registerMethod(Method method, Consumer methodHint) { + return registerType(TypeReference.of(method.getDeclaringClass()), + typeHint -> typeHint.withMethod(method.getName(), mapParameters(method), methodHint)); + } + + /** + * Register the need for reflection on the specified {@link Method}, + * enabling {@link ExecutableMode#INVOKE}. + * @param method the method that requires reflection + * @return {@code this}, to facilitate method chaining + */ + public ReflectionHints registerMethod(Method method) { + return registerMethod(method, methodHint -> methodHint.withMode(ExecutableMode.INVOKE)); + } + + private List mapParameters(Executable executable) { + return Arrays.stream(executable.getParameterTypes()).map(TypeReference::of) + .collect(Collectors.toList()); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/ReflectionTypeReference.java b/spring-core/src/main/java/org/springframework/core/hint/ReflectionTypeReference.java new file mode 100644 index 0000000000..85d8d05dc1 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/ReflectionTypeReference.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import org.springframework.lang.Nullable; + +/** + * A {@link TypeReference} based on a {@link Class}. + * + * @author Stephane Nicoll + */ +final class ReflectionTypeReference extends AbstractTypeReference { + + private final Class type; + + @Nullable + private final TypeReference enclosing; + + + private ReflectionTypeReference(Class type) { + this.type = type; + this.enclosing = (type.getEnclosingClass() != null + ? TypeReference.of(type.getEnclosingClass()) : null); + } + + static ReflectionTypeReference of(Class type) { + return new ReflectionTypeReference(type); + } + + @Override + public String getCanonicalName() { + return this.type.getCanonicalName(); + } + + @Override + public String getPackageName() { + return this.type.getPackageName(); + } + + @Override + public String getSimpleName() { + return this.type.getSimpleName(); + } + + @Override + public TypeReference getEnclosingType() { + return this.enclosing; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/ResourceBundleHint.java b/spring-core/src/main/java/org/springframework/core/hint/ResourceBundleHint.java new file mode 100644 index 0000000000..0586d3f4cb --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/ResourceBundleHint.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.util.ResourceBundle; + +/** + * A hint that describes the need to access to a {@link ResourceBundle}. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public class ResourceBundleHint { + + private final String baseName; + + + ResourceBundleHint(String baseName) { + this.baseName = baseName; + } + + /** + * Return the {@code baseName} of the resource bundle. + * @return the base name + */ + public String getBaseName() { + return this.baseName; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/ResourceHints.java b/spring-core/src/main/java/org/springframework/core/hint/ResourceHints.java new file mode 100644 index 0000000000..bfeb162ecc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/ResourceHints.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.springframework.core.hint.ResourcePatternHint.Builder; + +/** + * Gather the need for resources available at runtime. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public class ResourceHints { + + private final Set types; + + private final List resourcePatternHints; + + private final Set resourceBundleHints; + + + public ResourceHints() { + this.types = new HashSet<>(); + this.resourcePatternHints = new ArrayList<>(); + this.resourceBundleHints = new LinkedHashSet<>(); + } + + /** + * Return the resources that should be made available at runtime. + * @return a stream of {@link ResourcePatternHint} + */ + public Stream resourcePatterns() { + Stream patterns = this.resourcePatternHints.stream().map(Builder::build); + return (this.types.isEmpty() ? patterns + : Stream.concat(Stream.of(typesPatternResourceHint()), patterns)); + } + + /** + * Return the resource bundles that should be made available at runtime. + * @return a stream of {@link ResourceBundleHint} + */ + public Stream resourceBundles() { + return this.resourceBundleHints.stream().map(ResourceBundleHint::new); + } + + /** + * Register that the resources matching the specified pattern should be + * made available at runtime. + * @param include a pattern of the resources to include + * @param resourceHint a builder to further customize the resource pattern + * @return {@code this}, to facilitate method chaining + */ + public ResourceHints registerPattern(String include, Consumer resourceHint) { + Builder builder = new Builder().includes(include); + if (resourceHint != null) { + resourceHint.accept(builder); + } + this.resourcePatternHints.add(builder); + return this; + } + + /** + * Register that the resources matching the specified pattern should be + * made available at runtime. + * @param include a pattern of the resources to include + * @return {@code this}, to facilitate method chaining + */ + public ResourceHints registerPattern(String include) { + return registerPattern(include, null); + } + + /** + * Register that the bytecode of the type defined by the specified + * {@link TypeReference} should be made available at runtime. + * @param type the type to include + * @return {@code this}, to facilitate method chaining + */ + public ResourceHints registerType(TypeReference type) { + this.types.add(type); + return this; + } + + /** + * Register that the bytecode of the specified type should be made + * available at runtime. + * @param type the type to include + * @return {@code this}, to facilitate method chaining + */ + public ResourceHints registerType(Class type) { + return registerType(TypeReference.of(type)); + } + + /** + * Register that the resource bundle with the specified base name should + * be made available at runtime. + * @param baseName the base name of the resource bundle + * @return {@code this}, to facilitate method chaining + */ + public ResourceHints registerResourceBundle(String baseName) { + this.resourceBundleHints.add(baseName); + return this; + } + + private ResourcePatternHint typesPatternResourceHint() { + Builder builder = new Builder(); + this.types.forEach(type -> builder.includes(toIncludePattern(type))); + return builder.build(); + } + + private String toIncludePattern(TypeReference type) { + StringBuilder names = new StringBuilder(); + buildName(type, names); + String candidate = type.getPackageName() + "." + names; + return candidate.replace(".", "/") + ".class"; + } + + private void buildName(TypeReference type, StringBuilder sb) { + if (type == null) { + return; + } + String typeName = (type.getEnclosingType() != null) ? "$" + type.getSimpleName() : type.getSimpleName(); + sb.insert(0, typeName); + buildName(type.getEnclosingType(), sb); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/ResourcePatternHint.java b/spring-core/src/main/java/org/springframework/core/hint/ResourcePatternHint.java new file mode 100644 index 0000000000..87ae0c2587 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/ResourcePatternHint.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * A hint that describes resources that should be made available at runtime. + * + *

The patterns may be a simple path which has a one-to-one mapping to a + * resource on the classpath, or alternatively may contain the special + * {@code *} character to indicate a wildcard search. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public final class ResourcePatternHint { + + private final List includes; + + private final List excludes; + + + private ResourcePatternHint(Builder builder) { + this.includes = new ArrayList<>(builder.includes); + this.excludes = new ArrayList<>(builder.excludes); + } + + /** + * Return the include patterns to use to identify the resources to match. + * @return the include patterns + */ + public List getIncludes() { + return this.includes; + } + + /** + * Return the exclude patterns to use to identify the resources to match. + * @return the exclude patterns + */ + public List getExcludes() { + return this.excludes; + } + + + /** + * Builder for {@link ResourcePatternHint}. + */ + public static class Builder { + + private final Set includes = new LinkedHashSet<>(); + + private final Set excludes = new LinkedHashSet<>(); + + + /** + * Includes the resources matching the specified pattern. + * @param includes the include patterns + * @return {@code this}, to facilitate method chaining + */ + public Builder includes(String... includes) { + this.includes.addAll(Arrays.asList(includes)); + return this; + } + + /** + * Exclude resources matching the specified pattern. + * @param excludes the excludes pattern + * @return {@code this}, to facilitate method chaining + */ + public Builder excludes(String... excludes) { + this.excludes.addAll(Arrays.asList(excludes)); + return this; + } + + /** + * Creates a {@link ResourcePatternHint} based on the state of this + * builder. + * @return a resource pattern hint + */ + public ResourcePatternHint build() { + return new ResourcePatternHint(this); + } + + } +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/RuntimeHints.java b/spring-core/src/main/java/org/springframework/core/hint/RuntimeHints.java new file mode 100644 index 0000000000..0b869fd68e --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/RuntimeHints.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2022 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.core.hint; + +/** + * Gather hints that can be used to optimize the application runtime. + * + *

Use of reflection can be recorded for individual members of a type, as + * well as broader {@linkplain MemberCategory member categories}. Access to + * resources can be specified using patterns or the base name of a resource + * bundle. + * + *

Hints that require the need for Java serialization of proxies can be + * recorded as well. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public class RuntimeHints { + + private final ReflectionHints reflection = new ReflectionHints(); + + private final ResourceHints resources = new ResourceHints(); + + private final JavaSerializationHints javaSerialization = new JavaSerializationHints(); + + private final ProxyHints proxies = new ProxyHints(); + + + /** + * Provide access to reflection-based hints. + * @return reflection hints + */ + public ReflectionHints reflection() { + return this.reflection; + } + + /** + * Provide access to resource-based hints. + * @return resource hints + */ + public ResourceHints resources() { + return this.resources; + } + + /** + * Provide access to serialization-based hints. + * @return java serialization hints + */ + public JavaSerializationHints javaSerialization() { + return this.javaSerialization; + } + + /** + * Provide access to proxy-based hints. + * @return proxy hints + */ + public ProxyHints proxies() { + return this.proxies; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/SimpleTypeReference.java b/spring-core/src/main/java/org/springframework/core/hint/SimpleTypeReference.java new file mode 100644 index 0000000000..f19568c95c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/SimpleTypeReference.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A {@link TypeReference} based on fully qualified name. + * + * @author Stephane Nicoll + */ +final class SimpleTypeReference extends AbstractTypeReference { + + private String canonicalName; + + private final String packageName; + + private final String simpleName; + + @Nullable + private final TypeReference enclosingType; + + + SimpleTypeReference(String packageName, String simpleName, @Nullable TypeReference enclosingType) { + this.packageName = packageName; + this.simpleName = simpleName; + this.enclosingType = enclosingType; + } + + static SimpleTypeReference of(String className) { + Assert.notNull(className, "ClassName must not be null"); + if (!className.contains("$")) { + return createTypeReference(className); + } + String[] elements = className.split("\\$"); + SimpleTypeReference typeReference = createTypeReference(elements[0]); + for (int i = 1; i < elements.length; i++) { + typeReference = new SimpleTypeReference(typeReference.getPackageName(), elements[i], typeReference); + } + return typeReference; + } + + private static SimpleTypeReference createTypeReference(String className) { + int i = className.lastIndexOf('.'); + return new SimpleTypeReference(className.substring(0, i), className.substring(i + 1), null); + } + + @Override + public String getCanonicalName() { + if (this.canonicalName == null) { + StringBuilder names = new StringBuilder(); + buildName(this, names); + this.canonicalName = this.packageName + "." + names; + } + return this.canonicalName; + } + + private static void buildName(TypeReference type, StringBuilder sb) { + if (type == null) { + return; + } + String typeName = (type.getEnclosingType() != null) ? "." + type.getSimpleName() : type.getSimpleName(); + sb.insert(0, typeName); + buildName(type.getEnclosingType(), sb); + } + + @Override + public String getPackageName() { + return this.packageName; + } + + @Override + public String getSimpleName() { + return this.simpleName; + } + + @Override + public TypeReference getEnclosingType() { + return this.enclosingType; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/TypeHint.java b/spring-core/src/main/java/org/springframework/core/hint/TypeHint.java new file mode 100644 index 0000000000..16dfeddcf6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/TypeHint.java @@ -0,0 +1,253 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.util.Assert; + +/** + * A hint that describes the need for reflection on a type. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public final class TypeHint { + + private final TypeReference type; + + private final TypeReference reachableType; + + private final Set fields; + + private final Set constructors; + + private final Set methods; + + private final Set memberCategories; + + + private TypeHint(Builder builder) { + this.type = builder.type; + this.reachableType = builder.reachableType; + this.memberCategories = Set.copyOf(builder.memberCategories); + this.fields = builder.fields.values().stream().map(FieldHint.Builder::build).collect(Collectors.toSet()); + this.constructors = builder.constructors.values().stream().map(ExecutableHint.Builder::build).collect(Collectors.toSet()); + this.methods = builder.methods.values().stream().map(ExecutableHint.Builder::build).collect(Collectors.toSet()); + } + + /** + * Initialize a builder for the type defined by the specified + * {@link TypeReference}. + * @param type the type to use + * @return a builder + */ + public static Builder of(TypeReference type) { + Assert.notNull(type, "Type must not be null"); + return new Builder(type); + } + + /** + * Return the type that this hint handles. + * @return the type + */ + public TypeReference getType() { + return this.type; + } + + /** + * Return the type that should be reachable for this hint to apply, or + * {@code null} if this hint should always been applied. + * @return the reachable type, if any + */ + public TypeReference getReachableType() { + return this.reachableType; + } + + /** + * Return the fields that require reflection. + * @return a stream of {@link FieldHint} + */ + public Stream fields() { + return this.fields.stream(); + } + + /** + * Return the constructors that require reflection. + * @return a stream of {@link ExecutableHint} + */ + public Stream constructors() { + return this.constructors.stream(); + } + + /** + * Return the methods that require reflection. + * @return a stream of {@link ExecutableHint} + */ + public Stream methods() { + return this.methods.stream(); + } + + /** + * Return the member categories that apply. + * @return the member categories to enable + */ + public Set getMemberCategories() { + return this.memberCategories; + } + + + /** + * Builder for {@link TypeHint}. + */ + public static class Builder { + + private final TypeReference type; + + private TypeReference reachableType; + + private final Map fields = new HashMap<>(); + + private final Map constructors = new HashMap<>(); + + private final Map methods = new HashMap<>(); + + private final Set memberCategories = new HashSet<>(); + + + public Builder(TypeReference type) { + this.type = type; + } + + /** + * Make this hint conditional on the fact that the specified type + * can be resolved. + * @param reachableType the type that should be reachable for this + * hint to apply + * @return {@code this}, to facilitate method chaining + */ + public Builder onReachableType(TypeReference reachableType) { + this.reachableType = reachableType; + return this; + } + + /** + * Register the need for reflection on the field with the specified name. + * @param name the name of the field + * @param fieldHint a builder to further customize the hints of this field + * @return {@code this}, to facilitate method chaining + */ + public Builder withField(String name, Consumer fieldHint) { + FieldHint.Builder builder = this.fields.computeIfAbsent(name, FieldHint.Builder::new); + fieldHint.accept(builder); + return this; + } + + /** + * Register the need for reflection on the constructor with the specified + * parameter types. + * @param parameterTypes the parameter types of the constructor + * @param constructorHint a builder to further customize the hints of this + * constructor + * @return {@code this}, to facilitate method chaining + */ + public Builder withConstructor(List parameterTypes, Consumer constructorHint) { + ExecutableKey key = new ExecutableKey("", parameterTypes); + ExecutableHint.Builder builder = this.constructors.computeIfAbsent(key, + k -> ExecutableHint.ofConstructor(parameterTypes)); + constructorHint.accept(builder); + return this; + } + + /** + * Register the need for reflection on the method with the specified name + * and parameter types. + * @param name the name of the method + * @param parameterTypes the parameter types of the constructor + * @param methodHint a builder to further customize the hints of this method + * @return {@code this}, to facilitate method chaining + */ + public Builder withMethod(String name, List parameterTypes, Consumer methodHint) { + ExecutableKey key = new ExecutableKey(name, parameterTypes); + ExecutableHint.Builder builder = this.methods.computeIfAbsent(key, + k -> ExecutableHint.ofMethod(name, parameterTypes)); + methodHint.accept(builder); + return this; + } + + /** + * Adds the specified {@linkplain MemberCategory member categories}. + * @param memberCategories the categories to apply + * @return {@code this}, to facilitate method chaining + */ + public Builder withMembers(MemberCategory... memberCategories) { + this.memberCategories.addAll(Arrays.asList(memberCategories)); + return this; + } + + /** + * Create a {@link TypeHint} based on the state of this builder. + * @return a type hint + */ + public TypeHint build() { + return new TypeHint(this); + } + + } + + private static final class ExecutableKey { + + private final String name; + + private final List parameterTypes; + + + private ExecutableKey(String name, List parameterTypes) { + this.name = name; + this.parameterTypes = parameterTypes.stream().map(TypeReference::getCanonicalName) + .collect(Collectors.toList()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ExecutableKey that = (ExecutableKey) o; + return this.name.equals(that.name) && this.parameterTypes.equals(that.parameterTypes); + } + + @Override + public int hashCode() { + return Objects.hash(this.name, this.parameterTypes); + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/TypeReference.java b/spring-core/src/main/java/org/springframework/core/hint/TypeReference.java new file mode 100644 index 0000000000..f39260de82 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/TypeReference.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import org.springframework.lang.Nullable; + +/** + * Type abstraction that can be used to refer to types that are not available as + * a {@link Class} yet. + * + * @author Stephane Nicoll + * @since 6.0 + */ +public interface TypeReference { + + /** + * Return the {@linkplain Class#getCanonicalName() canonical name} of this + * type reference. + * @return the canonical name + */ + String getCanonicalName(); + + /** + * Return the package name of this type. + * @return the package name + */ + String getPackageName(); + + /** + * Return the {@linkplain Class#getSimpleName() simple name} of this type + * reference. + * @return the simple name + */ + String getSimpleName(); + + /** + * Return the enclosing type reference, or {@code null} if this type reference + * does not have an enclosing type. + * @return the enclosing type, if any + */ + @Nullable + TypeReference getEnclosingType(); + + /** + * Create an instance based on the specified type. + * @param type the type to wrap + * @return a type reference for the specified type + */ + static TypeReference of(Class type) { + return ReflectionTypeReference.of(type); + } + + /** + * Create an instance based on the specified class name. + * The format of the class name must follow {@linkplain Class#getName()}, + * in particular inner classes should be separated by a {@code $}. + * @param className the class name of the type to wrap + * @return a type reference for the specified class name + */ + static TypeReference of(String className) { + return SimpleTypeReference.of(className); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/hint/package-info.java b/spring-core/src/main/java/org/springframework/core/hint/package-info.java new file mode 100644 index 0000000000..8baf2b0d21 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/hint/package-info.java @@ -0,0 +1,5 @@ +/** + * Support for registering the need for reflection, resources, java serialization + * and proxies. + */ +package org.springframework.core.hint; diff --git a/spring-core/src/test/java/org/springframework/core/hint/ClassProxyHintTests.java b/spring-core/src/test/java/org/springframework/core/hint/ClassProxyHintTests.java new file mode 100644 index 0000000000..215f8bda60 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/hint/ClassProxyHintTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.io.Closeable; +import java.io.Serializable; +import java.util.Hashtable; +import java.util.Properties; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.hint.JdkProxyHint.Builder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ClassProxyHint}. + * + * @author Stephane Nicoll + */ +class ClassProxyHintTests { + + @Test + void equalsWithWithSameInstanceIsTrue() { + ClassProxyHint hint = ClassProxyHint.of(Properties.class).build(); + assertThat(hint).isEqualTo(hint); + } + + @Test + void equalsWithWithSameTargetClassIsTrue() { + ClassProxyHint first = ClassProxyHint.of(Properties.class).build(); + ClassProxyHint second = ClassProxyHint.of(TypeReference.of(Properties.class)).build(); + assertThat(first).isEqualTo(second); + } + + @Test + void equalsWithWithSameProxiedInterfacesIsTrue() { + ClassProxyHint first = ClassProxyHint.of(Properties.class) + .proxiedInterfaces(Serializable.class).build(); + ClassProxyHint second = ClassProxyHint.of(Properties.class) + .proxiedInterfaces(TypeReference.of(Serializable.class)).build(); + assertThat(first).isEqualTo(second); + } + + @Test + void equalsWithWithDifferentTargetClassIsFalse() { + ClassProxyHint first = ClassProxyHint.of(Properties.class).build(); + ClassProxyHint second = ClassProxyHint.of(Hashtable.class).build(); + assertThat(first).isNotEqualTo(second); + } + + @Test + void equalsWithWithSameProxiedInterfacesDifferentOrderIsFalse() { + ClassProxyHint first = ClassProxyHint.of(Properties.class) + .proxiedInterfaces(Serializable.class, Closeable.class).build(); + ClassProxyHint second = ClassProxyHint.of(Properties.class) + .proxiedInterfaces(TypeReference.of(Closeable.class), TypeReference.of(Serializable.class)) + .build(); + assertThat(first).isNotEqualTo(second); + } + + @Test + void equalsWithWithDifferentProxiedInterfacesIsFalse() { + ClassProxyHint first = ClassProxyHint.of(Properties.class) + .proxiedInterfaces(Serializable.class).build(); + ClassProxyHint second = ClassProxyHint.of(Properties.class) + .proxiedInterfaces(TypeReference.of(Closeable.class)).build(); + assertThat(first).isNotEqualTo(second); + } + + @Test + void equalsWithNonClassProxyHintIsFalse() { + ClassProxyHint first = ClassProxyHint.of(Properties.class).build(); + JdkProxyHint second = new Builder().proxiedInterfaces(Function.class).build(); + assertThat(first).isNotEqualTo(second); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/hint/JavaSerializationHintsTests.java b/spring-core/src/test/java/org/springframework/core/hint/JavaSerializationHintsTests.java new file mode 100644 index 0000000000..3eb632cdcf --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/hint/JavaSerializationHintsTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JavaSerializationHints}. + * + * @author Stephane Nicoll + */ +class JavaSerializationHintsTests { + + private final JavaSerializationHints javaSerializationHints = new JavaSerializationHints(); + + @Test + void registerTypeTwiceExposesOneHint() { + this.javaSerializationHints.registerType(URL.class); + this.javaSerializationHints.registerType(TypeReference.of(URL.class.getName())); + assertThat(this.javaSerializationHints.types()).singleElement() + .isEqualTo(TypeReference.of(URL.class)); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/hint/JdkProxyHintTests.java b/spring-core/src/test/java/org/springframework/core/hint/JdkProxyHintTests.java new file mode 100644 index 0000000000..89a7669dfd --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/hint/JdkProxyHintTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.util.function.Consumer; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.hint.JdkProxyHint.Builder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JdkProxyHint}. + * + * @author Stephane Nicoll + */ +class JdkProxyHintTests { + + @Test + void equalsWithWithSameInstanceIsTrue() { + JdkProxyHint hint = new Builder().proxiedInterfaces(Function.class, Consumer.class).build(); + assertThat(hint).isEqualTo(hint); + } + + @Test + void equalsWithWithSameProxiedInterfacesIsTrue() { + JdkProxyHint first = new Builder().proxiedInterfaces(Function.class, Consumer.class).build(); + JdkProxyHint second = new Builder().proxiedInterfaces(TypeReference.of(Function.class.getName()), + TypeReference.of(Consumer.class)).build(); + assertThat(first).isEqualTo(second); + } + + @Test + void equalsWithWithSameProxiedInterfacesDifferentOrderIsFalse() { + JdkProxyHint first = new Builder().proxiedInterfaces(Function.class, Consumer.class).build(); + JdkProxyHint second = new Builder().proxiedInterfaces(TypeReference.of(Consumer.class), + TypeReference.of(Function.class.getName())).build(); + assertThat(first).isNotEqualTo(second); + } + + @Test + void equalsWithWithDifferentProxiedInterfacesIsFalse() { + JdkProxyHint first = new Builder().proxiedInterfaces(Function.class).build(); + JdkProxyHint second = new Builder().proxiedInterfaces(TypeReference.of(Function.class.getName()), + TypeReference.of(Consumer.class)).build(); + assertThat(first).isNotEqualTo(second); + } + + @Test + void equalsWithNonJdkProxyHintIsFalse() { + JdkProxyHint first = new Builder().proxiedInterfaces(Function.class).build(); + TypeReference second = TypeReference.of(Function.class); + assertThat(first).isNotEqualTo(second); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/hint/ProxyHintsTests.java b/spring-core/src/test/java/org/springframework/core/hint/ProxyHintsTests.java new file mode 100644 index 0000000000..9fe36f0ded --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/hint/ProxyHintsTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Properties; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.hint.JdkProxyHint.Builder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ProxyHints}. + * + * @author Stephane Nicoll + */ +class ProxyHintsTests { + + private final ProxyHints proxyHints = new ProxyHints(); + + + @Test + void registerJdkProxyWithInterfaceClass() { + this.proxyHints.registerJdkProxy(Function.class); + assertThat(this.proxyHints.jdkProxies()).singleElement().satisfies(proxiedInterfaces(Function.class)); + } + + @Test + void registerJdkProxyWithConcreteClass() { + assertThatIllegalArgumentException().isThrownBy(() -> this.proxyHints.registerJdkProxy(String.class)) + .withMessageContaining(String.class.getName()); + } + + @Test + void registerJdkProxyWithInterfaceClassNames() { + this.proxyHints.registerJdkProxy(TypeReference.of(Function.class), + TypeReference.of("com.example.Advised")); + assertThat(this.proxyHints.jdkProxies()).singleElement().satisfies(proxiedInterfaces( + Function.class.getName(), "com.example.Advised")); + } + + @Test + void registerJdkProxyWithSupplier() { + this.proxyHints.registerJdkProxy(springProxy(TypeReference.of("com.example.Test"))); + assertThat(this.proxyHints.jdkProxies()).singleElement().satisfies(proxiedInterfaces( + "org.springframework.aop.SpringProxy", + "org.springframework.aop.framework.Advised", + "org.springframework.core.DecoratingProxy", + "com.example.Test")); + } + + @Test + void registerJdkProxyTwiceExposesOneHint() { + this.proxyHints.registerJdkProxy(Function.class); + this.proxyHints.registerJdkProxy(TypeReference.of(Function.class.getName())); + assertThat(this.proxyHints.jdkProxies()).singleElement().satisfies(proxiedInterfaces(Function.class)); + } + + @Test + void registerClassProxyWithTargetClass() { + this.proxyHints.registerClassProxy(Properties.class, classProxyHint -> + classProxyHint.proxiedInterfaces(Serializable.class)); + assertThat(this.proxyHints.classProxies()).singleElement().satisfies(classProxyHint -> { + assertThat(classProxyHint.getTargetClass()).isEqualTo(TypeReference.of(Properties.class)); + assertThat(classProxyHint.getProxiedInterfaces()).containsOnly(TypeReference.of(Serializable.class)); + }); + } + + @Test + void registerClassProxyWithTargetInterface() { + assertThatIllegalArgumentException().isThrownBy(() -> this.proxyHints.registerClassProxy(Serializable.class, classProxyHint -> { + })).withMessageContaining(Serializable.class.getName()); + } + + private static Supplier springProxy(TypeReference proxiedInterface) { + return () -> new Builder().proxiedInterfaces(Stream.of("org.springframework.aop.SpringProxy", + "org.springframework.aop.framework.Advised", "org.springframework.core.DecoratingProxy") + .map(TypeReference::of).toArray(TypeReference[]::new)) + .proxiedInterfaces(proxiedInterface).build(); + } + + private Consumer proxiedInterfaces(String... proxiedInterfaces) { + return jdkProxyHint -> assertThat(jdkProxyHint.getProxiedInterfaces()) + .containsExactly(Arrays.stream(proxiedInterfaces) + .map(TypeReference::of).toArray(TypeReference[]::new)); + } + + private Consumer proxiedInterfaces(Class... proxiedInterfaces) { + return jdkProxyHint -> assertThat(jdkProxyHint.getProxiedInterfaces()) + .containsExactly(Arrays.stream(proxiedInterfaces) + .map(TypeReference::of).toArray(TypeReference[]::new)); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/hint/ReflectionHintsTests.java b/spring-core/src/test/java/org/springframework/core/hint/ReflectionHintsTests.java new file mode 100644 index 0000000000..27460dcbf5 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/hint/ReflectionHintsTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2021 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.core.hint; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ReflectionHints}. + * + * @author Stephane Nicoll + */ +class ReflectionHintsTests { + + private final ReflectionHints reflectionHints = new ReflectionHints(); + + @Test + void registerType() { + this.reflectionHints.registerType(TypeReference.of(String.class), + hint -> hint.withMembers(MemberCategory.DECLARED_FIELDS)); + assertThat(this.reflectionHints.typeHints()).singleElement().satisfies( + typeWithMemberCategories(String.class, MemberCategory.DECLARED_FIELDS)); + } + + @Test + void registerTypeReuseBuilder() { + this.reflectionHints.registerType(TypeReference.of(String.class), + typeHint -> typeHint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); + this.reflectionHints.registerField(ReflectionUtils.findField(String.class, "value")); + assertThat(this.reflectionHints.typeHints()).singleElement().satisfies(typeHint -> { + assertThat(typeHint.getType().getCanonicalName()).isEqualTo(String.class.getCanonicalName()); + assertThat(typeHint.fields()).singleElement().satisfies(fieldHint -> assertThat(fieldHint.getName()).isEqualTo("value")); + assertThat(typeHint.getMemberCategories()).containsOnly(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + }); + } + + @Test + void registerClass() { + this.reflectionHints.registerType(Integer.class, + hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); + assertThat(this.reflectionHints.typeHints()).singleElement().satisfies( + typeWithMemberCategories(Integer.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); + } + + @Test + void registerField() { + this.reflectionHints.registerField(ReflectionUtils.findField(TestType.class, "field")); + assertThat(this.reflectionHints.typeHints()).singleElement().satisfies(typeHint -> { + assertThat(typeHint.getType().getCanonicalName()).isEqualTo(TestType.class.getCanonicalName()); + assertThat(typeHint.fields()).singleElement().satisfies(fieldHint -> + assertThat(fieldHint.getName()).isEqualTo("field")); + assertThat(typeHint.constructors()).isEmpty(); + assertThat(typeHint.methods()).isEmpty(); + assertThat(typeHint.getMemberCategories()).isEmpty(); + }); + } + + @Test + void registerConstructor() { + this.reflectionHints.registerConstructor(TestType.class.getDeclaredConstructors()[0]); + assertThat(this.reflectionHints.typeHints()).singleElement().satisfies(typeHint -> { + assertThat(typeHint.getMemberCategories()).isEmpty(); + assertThat(typeHint.getType().getCanonicalName()).isEqualTo(TestType.class.getCanonicalName()); + assertThat(typeHint.fields()).isEmpty(); + assertThat(typeHint.constructors()).singleElement().satisfies(constructorHint -> { + assertThat(constructorHint.getParameterTypes()).isEmpty(); + assertThat(constructorHint.getModes()).containsOnly(ExecutableMode.INVOKE); + }); + assertThat(typeHint.methods()).isEmpty(); + assertThat(typeHint.getMemberCategories()).isEmpty(); + }); + } + + @Test + void registerMethod() { + this.reflectionHints.registerMethod(ReflectionUtils.findMethod(TestType.class, "setName", String.class)); + assertThat(this.reflectionHints.typeHints()).singleElement().satisfies(typeHint -> { + assertThat(typeHint.getType().getCanonicalName()).isEqualTo(TestType.class.getCanonicalName()); + assertThat(typeHint.fields()).isEmpty(); + assertThat(typeHint.constructors()).isEmpty(); + assertThat(typeHint.methods()).singleElement().satisfies(methodHint -> { + assertThat(methodHint.getName()).isEqualTo("setName"); + assertThat(methodHint.getParameterTypes()).containsOnly(TypeReference.of(String.class)); + assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INVOKE); + }); + }); + } + + private Consumer typeWithMemberCategories(Class type, MemberCategory... memberCategories) { + return typeHint -> { + assertThat(typeHint.getType().getCanonicalName()).isEqualTo(type.getCanonicalName()); + assertThat(typeHint.fields()).isEmpty(); + assertThat(typeHint.constructors()).isEmpty(); + assertThat(typeHint.methods()).isEmpty(); + assertThat(typeHint.getMemberCategories()).containsExactly(memberCategories); + }; + } + + + @SuppressWarnings("unused") + static class TestType { + + private String field; + + void setName(String name) { + + } + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/hint/ResourceHintsTests.java b/spring-core/src/test/java/org/springframework/core/hint/ResourceHintsTests.java new file mode 100644 index 0000000000..9ec2ed129b --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/hint/ResourceHintsTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2021 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.core.hint; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.hint.ResourceHintsTests.Nested.Inner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ResourceHints}. + * + * @author Stephane Nicoll + */ +class ResourceHintsTests { + + private final ResourceHints resourceHints = new ResourceHints(); + + @Test + void registerType() { + this.resourceHints.registerType(String.class); + assertThat(this.resourceHints.resourcePatterns()).singleElement().satisfies( + patternOf("java/lang/String.class")); + } + + @Test + void registerTypeWithNestedType() { + this.resourceHints.registerType(TypeReference.of(Nested.class)); + assertThat(this.resourceHints.resourcePatterns()).singleElement().satisfies( + patternOf("org/springframework/core/hint/ResourceHintsTests$Nested.class")); + } + + @Test + void registerTypeWithInnerNestedType() { + this.resourceHints.registerType(TypeReference.of(Inner.class)); + assertThat(this.resourceHints.resourcePatterns()).singleElement().satisfies( + patternOf("org/springframework/core/hint/ResourceHintsTests$Nested$Inner.class")); + } + + @Test + void registerTypeSeveralTimesAddsOnlyOneEntry() { + this.resourceHints.registerType(String.class); + this.resourceHints.registerType(TypeReference.of(String.class)); + assertThat(this.resourceHints.resourcePatterns()).singleElement().satisfies( + patternOf("java/lang/String.class")); + } + + @Test + void registerExactMatch() { + this.resourceHints.registerPattern("com/example/test.properties"); + this.resourceHints.registerPattern("com/example/another.properties"); + assertThat(this.resourceHints.resourcePatterns()) + .anySatisfy(patternOf("com/example/test.properties")) + .anySatisfy(patternOf("com/example/another.properties")) + .hasSize(2); + } + + @Test + void registerPattern() { + this.resourceHints.registerPattern("com/example/*.properties"); + assertThat(this.resourceHints.resourcePatterns()).singleElement().satisfies( + patternOf("com/example/*.properties")); + } + + @Test + void registerPatternWithIncludesAndExcludes() { + this.resourceHints.registerPattern("com/example/*.properties", + resourceHint -> resourceHint.excludes("com/example/to-ignore.properties")); + assertThat(this.resourceHints.resourcePatterns()).singleElement().satisfies(patternOf( + List.of("com/example/*.properties"), + List.of("com/example/to-ignore.properties"))); + } + + @Test + void registerResourceBundle() { + this.resourceHints.registerResourceBundle("com.example.message"); + assertThat(this.resourceHints.resourceBundles()).singleElement() + .satisfies(resourceBundle("com.example.message")); + } + + @Test + void registerResourceBundleSeveralTimesAddsOneEntry() { + this.resourceHints.registerResourceBundle("com.example.message") + .registerResourceBundle("com.example.message"); + assertThat(this.resourceHints.resourceBundles()).singleElement() + .satisfies(resourceBundle("com.example.message")); + } + + + private Consumer patternOf(String... includes) { + return patternOf(Arrays.asList(includes), Collections.emptyList()); + } + + private Consumer resourceBundle(String baseName) { + return resourceBundleHint -> assertThat(resourceBundleHint.getBaseName()).isEqualTo(baseName); + } + + private Consumer patternOf(List includes, List excludes) { + return pattern -> { + assertThat(pattern.getIncludes()).containsExactlyElementsOf(includes); + assertThat(pattern.getExcludes()).containsExactlyElementsOf(excludes); + }; + } + + static class Nested { + + static class Inner { + + } + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/hint/RuntimeHintsTests.java b/spring-core/src/test/java/org/springframework/core/hint/RuntimeHintsTests.java new file mode 100644 index 0000000000..46cec4a50c --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/hint/RuntimeHintsTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2021 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.core.hint; + +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RuntimeHints}. + * + * @author Stephane Nicoll + */ +class RuntimeHintsTests { + + private final RuntimeHints hints = new RuntimeHints(); + + @Test + void reflectionHintWithClass() { + this.hints.reflection().registerType(String.class, + hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); + assertThat(this.hints.reflection().typeHints()).singleElement().satisfies(typeHint -> { + assertThat(typeHint.getType().getCanonicalName()).isEqualTo(String.class.getCanonicalName()); + assertThat(typeHint.fields()).isEmpty(); + assertThat(typeHint.constructors()).isEmpty(); + assertThat(typeHint.methods()).isEmpty(); + assertThat(typeHint.getMemberCategories()).containsOnly(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); + }); + } + + @Test + void resourceHintWithClass() { + this.hints.resources().registerType(String.class); + assertThat(this.hints.resources().resourcePatterns()).singleElement().satisfies(resourceHint -> { + assertThat(resourceHint.getIncludes()).containsExactly("java/lang/String.class"); + assertThat(resourceHint.getExcludes()).isEmpty(); + }); + } + + @Test + void javaSerializationHintWithClass() { + this.hints.javaSerialization().registerType(String.class); + assertThat(this.hints.javaSerialization().types()).containsExactly(TypeReference.of(String.class)); + } + + @Test + void jdkProxyWithClass() { + this.hints.proxies().registerJdkProxy(Function.class); + assertThat(this.hints.proxies().jdkProxies()).singleElement().satisfies(jdkProxyHint -> + assertThat(jdkProxyHint.getProxiedInterfaces()).containsExactly(TypeReference.of(Function.class))); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/hint/TypeHintTests.java b/spring-core/src/test/java/org/springframework/core/hint/TypeHintTests.java new file mode 100644 index 0000000000..3916c74473 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/hint/TypeHintTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2002-2021 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.core.hint; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.hint.TypeHint.Builder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link TypeHint}. + * + * @author Stephane Nicoll + */ +class TypeHintTests { + + @Test + void createWithNullTypeReference() { + assertThatIllegalArgumentException().isThrownBy(() -> TypeHint.of(null)); + } + + @Test + void createWithType() { + TypeHint hint = TypeHint.of(TypeReference.of(String.class)).build(); + assertThat(hint).isNotNull(); + assertThat(hint.getType().getCanonicalName()).isEqualTo("java.lang.String"); + } + + @Test + void createWithTypeAndReachableType() { + TypeHint hint = TypeHint.of(TypeReference.of(String.class)) + .onReachableType(TypeReference.of("com.example.Test")).build(); + assertThat(hint).isNotNull(); + assertThat(hint.getReachableType()).isNotNull(); + assertThat(hint.getReachableType().getCanonicalName()).isEqualTo("com.example.Test"); + } + + @Test + void createWithField() { + TypeHint hint = TypeHint.of(TypeReference.of(String.class)) + .withField("value", fieldHint -> fieldHint.allowWrite(true)).build(); + assertThat(hint.fields()).singleElement().satisfies(fieldHint -> { + assertThat(fieldHint.getName()).isEqualTo("value"); + assertThat(fieldHint.isAllowWrite()).isTrue(); + assertThat(fieldHint.isAllowUnsafeAccess()).isFalse(); + }); + } + + @Test + void createWithFieldReuseBuilder() { + Builder builder = TypeHint.of(TypeReference.of(String.class)); + builder.withField("value", fieldHint -> fieldHint.allowUnsafeAccess(true)); + builder.withField("value", fieldHint -> { + fieldHint.allowWrite(true); + fieldHint.allowUnsafeAccess(false); + }); + TypeHint hint = builder.build(); + assertThat(hint.fields()).singleElement().satisfies(fieldHint -> { + assertThat(fieldHint.getName()).isEqualTo("value"); + assertThat(fieldHint.isAllowWrite()).isTrue(); + assertThat(fieldHint.isAllowUnsafeAccess()).isFalse(); + }); + } + + @Test + void createWithConstructor() { + List parameterTypes = List.of(TypeReference.of(byte[].class), TypeReference.of(int.class)); + TypeHint hint = TypeHint.of(TypeReference.of(String.class)).withConstructor(parameterTypes, + constructorHint -> constructorHint.withMode(ExecutableMode.INVOKE)).build(); + assertThat(hint.constructors()).singleElement().satisfies(constructorHint -> { + assertThat(constructorHint.getParameterTypes()).containsOnlyOnceElementsOf(parameterTypes); + assertThat(constructorHint.getModes()).containsOnly(ExecutableMode.INVOKE); + }); + } + + @Test + void createConstructorReuseBuilder() { + List parameterTypes = List.of(TypeReference.of(byte[].class), TypeReference.of(int.class)); + Builder builder = TypeHint.of(TypeReference.of(String.class)).withConstructor(parameterTypes, + constructorHint -> constructorHint.withMode(ExecutableMode.INVOKE)); + TypeHint hint = builder.withConstructor(parameterTypes, constructorHint -> + constructorHint.withMode(ExecutableMode.INTROSPECT)).build(); + assertThat(hint.constructors()).singleElement().satisfies(constructorHint -> { + assertThat(constructorHint.getParameterTypes()).containsExactlyElementsOf(parameterTypes); + assertThat(constructorHint.getModes()).containsOnly(ExecutableMode.INVOKE, ExecutableMode.INTROSPECT); + }); + } + + @Test + void createWithMethod() { + List parameterTypes = List.of(TypeReference.of(char[].class)); + TypeHint hint = TypeHint.of(TypeReference.of(String.class)).withMethod("valueOf", parameterTypes, + methodHint -> methodHint.withMode(ExecutableMode.INVOKE)).build(); + assertThat(hint.methods()).singleElement().satisfies(methodHint -> { + assertThat(methodHint.getName()).isEqualTo("valueOf"); + assertThat(methodHint.getParameterTypes()).containsExactlyElementsOf(parameterTypes); + assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INVOKE); + }); + } + + @Test + void createWithMethodReuseBuilder() { + List parameterTypes = List.of(TypeReference.of(char[].class)); + Builder builder = TypeHint.of(TypeReference.of(String.class)).withMethod("valueOf", parameterTypes, + methodHint -> methodHint.withMode(ExecutableMode.INVOKE)); + TypeHint hint = builder.withMethod("valueOf", parameterTypes, + methodHint -> methodHint.setModes(ExecutableMode.INTROSPECT)).build(); + assertThat(hint.methods()).singleElement().satisfies(methodHint -> { + assertThat(methodHint.getName()).isEqualTo("valueOf"); + assertThat(methodHint.getParameterTypes()).containsExactlyElementsOf(parameterTypes); + assertThat(methodHint.getModes()).containsOnly(ExecutableMode.INTROSPECT); + }); + } + + @Test + void createWithMemberCategory() { + TypeHint hint = TypeHint.of(TypeReference.of(String.class)) + .withMembers(MemberCategory.DECLARED_FIELDS).build(); + assertThat(hint.getMemberCategories()).containsOnly(MemberCategory.DECLARED_FIELDS); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/hint/TypeReferenceTests.java b/spring-core/src/test/java/org/springframework/core/hint/TypeReferenceTests.java new file mode 100644 index 0000000000..e60dfd3d49 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/hint/TypeReferenceTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2022 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.core.hint; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TypeReference}. + * + * @author Stephane Nicoll + */ +class TypeReferenceTests { + + @Test + void typeReferenceWithClassName() { + TypeReference type = TypeReference.of("java.lang.String"); + assertThat(type.getCanonicalName()).isEqualTo("java.lang.String"); + assertThat(type.getPackageName()).isEqualTo("java.lang"); + assertThat(type.getSimpleName()).isEqualTo("String"); + assertThat(type.getEnclosingType()).isNull(); + } + + @Test + void typeReferenceWithInnerClassName() { + TypeReference type = TypeReference.of("com.example.Example$Inner"); + assertThat(type.getCanonicalName()).isEqualTo("com.example.Example.Inner"); + assertThat(type.getPackageName()).isEqualTo("com.example"); + assertThat(type.getSimpleName()).isEqualTo("Inner"); + assertThat(type.getEnclosingType()).satisfies(enclosingType -> { + assertThat(enclosingType.getCanonicalName()).isEqualTo("com.example.Example"); + assertThat(enclosingType.getPackageName()).isEqualTo("com.example"); + assertThat(enclosingType.getSimpleName()).isEqualTo("Example"); + assertThat(enclosingType.getEnclosingType()).isNull(); + }); + } + + @Test + void typeReferenceWithNestedInnerClassName() { + TypeReference type = TypeReference.of("com.example.Example$Inner$Nested"); + assertThat(type.getCanonicalName()).isEqualTo("com.example.Example.Inner.Nested"); + assertThat(type.getPackageName()).isEqualTo("com.example"); + assertThat(type.getSimpleName()).isEqualTo("Nested"); + assertThat(type.getEnclosingType()).satisfies(enclosingType -> { + assertThat(enclosingType.getCanonicalName()).isEqualTo("com.example.Example.Inner"); + assertThat(enclosingType.getPackageName()).isEqualTo("com.example"); + assertThat(enclosingType.getSimpleName()).isEqualTo("Inner"); + assertThat(enclosingType.getEnclosingType()).satisfies(parentEnclosingType -> { + assertThat(parentEnclosingType.getCanonicalName()).isEqualTo("com.example.Example"); + assertThat(parentEnclosingType.getPackageName()).isEqualTo("com.example"); + assertThat(parentEnclosingType.getSimpleName()).isEqualTo("Example"); + assertThat(parentEnclosingType.getEnclosingType()).isNull(); + }); + }); + } + + @Test + void equalsWithIdenticalNameIsTrue() { + assertThat(TypeReference.of(String.class)).isEqualTo( + TypeReference.of("java.lang.String")); + } + + @Test + void equalsWithNonTypeReferenceIsFalse() { + assertThat(TypeReference.of(String.class)).isNotEqualTo("java.lang.String"); + } + + @Test + void toStringUsesCanonicalName() { + assertThat(TypeReference.of(String.class)).hasToString("java.lang.String"); + } + +}