From a605d3f6ed7c6444cb1d3420e80130f062a5aeab Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 13 Apr 2022 17:38:40 -0700 Subject: [PATCH] Add AccessVisibility detection support Add `AccessVisibility` enum which can be used to determine the access visibility of `Member` or `ResolvableType`. See gh-28414 --- .../aot/generate/AccessVisibility.java | 186 ++++++++++++++++++ .../aot/generate/AccessVisibilityTests.java | 175 ++++++++++++++++ .../aot/generate/PackagePrivateClass.java | 29 +++ .../aot/generate/ProtectedAccessor.java | 32 +++ .../aot/generate/PublicClass.java | 34 ++++ 5 files changed, 456 insertions(+) create mode 100644 spring-core/src/main/java/org/springframework/aot/generate/AccessVisibility.java create mode 100644 spring-core/src/test/java/org/springframework/aot/generate/AccessVisibilityTests.java create mode 100644 spring-core/src/test/java/org/springframework/aot/generate/PackagePrivateClass.java create mode 100644 spring-core/src/test/java/org/springframework/aot/generate/ProtectedAccessor.java create mode 100644 spring-core/src/test/java/org/springframework/aot/generate/PublicClass.java diff --git a/spring-core/src/main/java/org/springframework/aot/generate/AccessVisibility.java b/spring-core/src/main/java/org/springframework/aot/generate/AccessVisibility.java new file mode 100644 index 00000000000..1233a949c31 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/AccessVisibility.java @@ -0,0 +1,186 @@ +/* + * 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.aot.generate; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashSet; +import java.util.Set; +import java.util.function.IntFunction; + +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Access visibility types as determined by the modifiers + * on a {@link Member} or {@link ResolvableType}. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + * @see #forMember(Member) + * @see #forResolvableType(ResolvableType) + */ +public enum AccessVisibility { + + /** + * Public visibility. The member or type is visible to all classes. + */ + PUBLIC, + + /** + * Protected visibility. The member or type is only visible to subclasses. + */ + PROTECTED, + + /** + * Package-private visibility. The member or type is only visible to classes + * in the same package. + */ + PACKAGE_PRIVATE, + + /** + * Private visibility. The member or type is not visible to other classes. + */ + PRIVATE; + + + /** + * Determine the {@link AccessVisibility} for the given member. This method + * will consider the member modifier, parameter types, return types and any + * enclosing classes. The lowest overall visibility will be returned. + * @param member the source member + * @return the {@link AccessVisibility} for the member + */ + public static AccessVisibility forMember(Member member) { + Assert.notNull(member, "'member' must not be null"); + AccessVisibility visibility = forModifiers(member.getModifiers()); + AccessVisibility declaringClassVisibility = forClass(member.getDeclaringClass()); + visibility = lowest(visibility, declaringClassVisibility); + if (visibility != PRIVATE) { + if (member instanceof Field field) { + AccessVisibility fieldVisibility = forResolvableType( + ResolvableType.forField(field)); + return lowest(visibility, fieldVisibility); + } + if (member instanceof Constructor constructor) { + AccessVisibility parameterVisibility = forParameterTypes(constructor, + i -> ResolvableType.forConstructorParameter(constructor, i)); + return lowest(visibility, parameterVisibility); + } + if (member instanceof Method method) { + AccessVisibility parameterVisibility = forParameterTypes(method, + i -> ResolvableType.forMethodParameter(method, i)); + AccessVisibility returnTypeVisibility = forResolvableType( + ResolvableType.forMethodReturnType(method)); + return lowest(visibility, parameterVisibility, returnTypeVisibility); + } + } + return PRIVATE; + } + + /** + * Determine the {@link AccessVisibility} for the given + * {@link ResolvableType}. This method will consider the type itself as well + * as any generics. + * @param resolvableType the source resolvable type + * @return the {@link AccessVisibility} for the type + */ + public static AccessVisibility forResolvableType(ResolvableType resolvableType) { + return forResolvableType(resolvableType, new HashSet<>()); + } + + @Nullable + private static AccessVisibility forResolvableType(ResolvableType resolvableType, + Set seen) { + if (!seen.add(resolvableType)) { + return AccessVisibility.PUBLIC; + } + Class userClass = ClassUtils.getUserClass(resolvableType.toClass()); + ResolvableType userType = resolvableType.as(userClass); + AccessVisibility visibility = forClass(userType.toClass()); + for (ResolvableType generic : userType.getGenerics()) { + visibility = lowest(visibility, forResolvableType(generic, seen)); + } + return visibility; + } + + private static AccessVisibility forParameterTypes(Executable executable, + IntFunction resolvableTypeFactory) { + AccessVisibility visibility = AccessVisibility.PUBLIC; + Class[] parameterTypes = executable.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + ResolvableType type = resolvableTypeFactory.apply(i); + visibility = lowest(visibility, forResolvableType(type)); + } + return visibility; + } + + /** + * Determine the {@link AccessVisibility} for the given {@link Class}. + * @param clazz the source class + * @return the {@link AccessVisibility} for the class + */ + public static AccessVisibility forClass(Class clazz) { + clazz = ClassUtils.getUserClass(clazz); + AccessVisibility visibility = forModifiers(clazz.getModifiers()); + if (clazz.isArray()) { + visibility = lowest(visibility, forClass(clazz.getComponentType())); + } + Class enclosingClass = clazz.getEnclosingClass(); + if (enclosingClass != null) { + visibility = lowest(visibility, forClass(clazz.getEnclosingClass())); + } + return visibility; + } + + private static AccessVisibility forModifiers(int modifiers) { + if (Modifier.isPublic(modifiers)) { + return PUBLIC; + } + if (Modifier.isProtected(modifiers)) { + return PROTECTED; + } + if (Modifier.isPrivate(modifiers)) { + return PRIVATE; + } + return PACKAGE_PRIVATE; + } + + /** + * Returns the lowest {@link AccessVisibility} put of the given candidates. + * @param candidates the candidates to check + * @return the lowest {@link AccessVisibility} from the candidates + */ + public static AccessVisibility lowest(AccessVisibility... candidates) { + AccessVisibility visibility = PUBLIC; + for (AccessVisibility candidate : candidates) { + if (candidate.ordinal() > visibility.ordinal()) { + visibility = candidate; + } + } + return visibility; + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/AccessVisibilityTests.java b/spring-core/src/test/java/org/springframework/aot/generate/AccessVisibilityTests.java new file mode 100644 index 00000000000..0b45d6827e9 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/AccessVisibilityTests.java @@ -0,0 +1,175 @@ +/* + * 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.aot.generate; + +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.ResolvableType; +import org.springframework.core.testfixture.aot.generator.visibility.ProtectedGenericParameter; +import org.springframework.core.testfixture.aot.generator.visibility.ProtectedParameter; +import org.springframework.core.testfixture.aot.generator.visibility.PublicFactoryBean; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AccessVisibility}. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +class AccessVisibilityTests { + + @Test + void forMemberWhenPublicConstructor() throws NoSuchMethodException { + Member member = PublicClass.class.getConstructor(); + assertThat(AccessVisibility.forMember(member)).isEqualTo(AccessVisibility.PUBLIC); + } + + @Test + void forMemberWhenPackagePrivateConstructor() { + Member member = ProtectedAccessor.class.getDeclaredConstructors()[0]; + assertThat(AccessVisibility.forMember(member)) + .isEqualTo(AccessVisibility.PACKAGE_PRIVATE); + } + + @Test + void forMemberWhenPackagePrivateClassWithPublicConstructor() { + Member member = PackagePrivateClass.class.getDeclaredConstructors()[0]; + assertThat(AccessVisibility.forMember(member)) + .isEqualTo(AccessVisibility.PACKAGE_PRIVATE); + } + + @Test + void forMemberWhenPackagePrivateClassWithPublicMethod() { + Member member = method(PackagePrivateClass.class, "stringBean"); + assertThat(AccessVisibility.forMember(member)) + .isEqualTo(AccessVisibility.PACKAGE_PRIVATE); + } + + @Test + void forMemberWhenPublicClassWithPackagePrivateConstructorParameter() { + Member member = ProtectedParameter.class.getConstructors()[0]; + assertThat(AccessVisibility.forMember(member)) + .isEqualTo(AccessVisibility.PACKAGE_PRIVATE); + } + + @Test + void forMemberWhenPublicClassWithPackagePrivateGenericOnConstructorParameter() { + Member member = ProtectedGenericParameter.class.getConstructors()[0]; + assertThat(AccessVisibility.forMember(member)) + .isEqualTo(AccessVisibility.PACKAGE_PRIVATE); + } + + @Test + void forMemberWhenPublicClassWithPackagePrivateMethod() { + Member member = method(PublicClass.class, "getProtectedMethod"); + assertThat(AccessVisibility.forMember(member)) + .isEqualTo(AccessVisibility.PACKAGE_PRIVATE); + } + + @Test + void forMemberWhenPublicClassWithPackagePrivateMethodReturnType() { + Member member = method(ProtectedAccessor.class, "methodWithProtectedReturnType"); + assertThat(AccessVisibility.forMember(member)) + .isEqualTo(AccessVisibility.PACKAGE_PRIVATE); + } + + @Test + void forMemberWhenPublicClassWithPackagePrivateMethodParameter() { + Member member = method(ProtectedAccessor.class, "methodWithProtectedParameter", + PackagePrivateClass.class); + assertThat(AccessVisibility.forMember(member)) + .isEqualTo(AccessVisibility.PACKAGE_PRIVATE); + } + + @Test + void forMemberWhenPublicClassWithPackagePrivateField() { + Field member = field(PublicClass.class, "protectedField"); + assertThat(AccessVisibility.forMember(member)) + .isEqualTo(AccessVisibility.PACKAGE_PRIVATE); + } + + @Test + void forMemberWhenPublicClassWithPublicFieldAndPackagePrivateFieldType() { + Member member = field(PublicClass.class, "protectedClassField"); + assertThat(AccessVisibility.forMember(member)) + .isEqualTo(AccessVisibility.PACKAGE_PRIVATE); + } + + @Test + void forMemberWhenPublicClassWithPublicMethodAndPackagePrivateGenericOnReturnType() { + Member member = method(PublicFactoryBean.class, "protectedTypeFactoryBean"); + assertThat(AccessVisibility.forMember(member)) + .isEqualTo(AccessVisibility.PACKAGE_PRIVATE); + } + + @Test + void forMemberWhenPublicClassWithPackagePrivateArrayComponent() { + Member member = field(PublicClass.class, "packagePrivateClasses"); + assertThat(AccessVisibility.forMember(member)) + .isEqualTo(AccessVisibility.PACKAGE_PRIVATE); + } + + @Test + void forResolvableTypeWhenPackagePrivateGeneric() { + ResolvableType resolvableType = PublicFactoryBean + .resolveToProtectedGenericParameter(); + assertThat(AccessVisibility.forResolvableType(resolvableType)) + .isEqualTo(AccessVisibility.PACKAGE_PRIVATE); + } + + @Test + void forResolvableTypeWhenRecursiveType() { + ResolvableType resolvableType = ResolvableType + .forClassWithGenerics(SelfReference.class, SelfReference.class); + assertThat(AccessVisibility.forResolvableType(resolvableType)) + .isEqualTo(AccessVisibility.PACKAGE_PRIVATE); + } + + @Test + void forMemberWhenPublicClassWithPrivateField() { + Member member = field(PublicClass.class, "privateField"); + assertThat(AccessVisibility.forMember(member)) + .isEqualTo(AccessVisibility.PRIVATE); + } + + private static Method method(Class type, String name, Class... parameterTypes) { + Method method = ReflectionUtils.findMethod(type, name, parameterTypes); + assertThat(method).isNotNull(); + return method; + } + + private static Field field(Class type, String name) { + Field field = ReflectionUtils.findField(type, name); + assertThat(field).isNotNull(); + return field; + } + + static class SelfReference> { + + @SuppressWarnings("unchecked") + T getThis() { + return (T) this; + } + + } +} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/PackagePrivateClass.java b/spring-core/src/test/java/org/springframework/aot/generate/PackagePrivateClass.java new file mode 100644 index 00000000000..cde4dd15ca2 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/PackagePrivateClass.java @@ -0,0 +1,29 @@ +/* + * 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.aot.generate; + +@SuppressWarnings("unused") +class PackagePrivateClass { + + public PackagePrivateClass() { + } + + public String stringBean() { + return "public"; + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/ProtectedAccessor.java b/spring-core/src/test/java/org/springframework/aot/generate/ProtectedAccessor.java new file mode 100644 index 00000000000..ada7e595f53 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/ProtectedAccessor.java @@ -0,0 +1,32 @@ +/* + * 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.aot.generate; + +@SuppressWarnings("unused") +public class ProtectedAccessor { + + ProtectedAccessor() { + } + + public String methodWithProtectedParameter(PackagePrivateClass type) { + return "test"; + } + + public PackagePrivateClass methodWithProtectedReturnType() { + return new PackagePrivateClass(); + } +} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/PublicClass.java b/spring-core/src/test/java/org/springframework/aot/generate/PublicClass.java new file mode 100644 index 00000000000..39d8d1e5188 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/PublicClass.java @@ -0,0 +1,34 @@ +/* + * 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.aot.generate; + +@SuppressWarnings("unused") +public class PublicClass { + + private String privateField; + + String protectedField; + + public PackagePrivateClass[] packagePrivateClasses; + + public PackagePrivateClass protectedClassField; + + String getProtectedMethod() { + return this.protectedField; + } + +}