Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/4ISSpVK4)
**Java reflection**

Ваша задача --- написать генератор экземпляров произвольных классов.
Expand Down
8 changes: 8 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@ plugins {
group = "org.example"
version = "1.0-SNAPSHOT"

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}

repositories {
mavenCentral()
}

dependencies {
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.assertj:assertj-core:3.27.6")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("net.bytebuddy:byte-buddy:1.17.8")
}

tasks.test {
Expand Down
17 changes: 0 additions & 17 deletions src/main/java/org/example/GenerateExample.java

This file was deleted.

11 changes: 11 additions & 0 deletions src/main/java/org/example/generator/Generatable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.example.generator;

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

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Generatable {
}
7 changes: 7 additions & 0 deletions src/main/java/org/example/generator/GenerationException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.example.generator;

public class GenerationException extends Exception {
public GenerationException(String message) {
super(message);
}
}
298 changes: 293 additions & 5 deletions src/main/java/org/example/generator/Generator.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,306 @@
package org.example.generator;

import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Random;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URISyntaxException;
import java.util.*;
import java.util.function.Supplier;
import org.example.generator.type.TypeGeneratorsProvider;

public class Generator {

public Object generateValueOfType(Class<?> clazz) throws InvocationTargetException, InstantiationException, IllegalAccessException {
private final Map<Class<?>, Supplier<?>> generators;

private final int maxDepth;
private final String packageToScan;
private final Set<Class<?>> classesInPackageToScan;

private final Random random = new Random();

public Generator(
Collection<TypeGeneratorsProvider> providers,
int maxDepth,
Object packageMarker
) {
Map<Class<?>, Supplier<?>> result = new HashMap<>();

for (TypeGeneratorsProvider provider : providers) {
Map<Class<?>, Supplier<?>> generatorsFromProvider = provider.getGenerators();

for (Map.Entry<Class<?>, Supplier<?>> entry : generatorsFromProvider.entrySet()) {
Class<?> type = entry.getKey();
Supplier<?> supplier = entry.getValue();

if (result.containsKey(type)) {
throw new IllegalArgumentException(
"Multiple providers supply generator for type: " + type.getName()
);
}

result.put(type, supplier);
}
}

this.generators = Map.copyOf(result);

if (maxDepth <= 0) {
throw new IllegalArgumentException("maxDepth expected to be more than 0, but got " + maxDepth);
}
this.maxDepth = maxDepth;

this.packageToScan = packageMarker.getClass().getPackageName();
// todo add test
try {
this.classesInPackageToScan = PackageUtils.getClassesInPackage(
packageToScan,
Thread.currentThread().getContextClassLoader()
);
} catch (IOException | URISyntaxException e) {
throw new RuntimeException(e);
}
}

public Object generateValueOfType(
Class<?> clazz
) throws InvocationTargetException, InstantiationException, IllegalAccessException, GenerationException {
return generateValueOfType(clazz, 0);
}

private Object generateValueOfType(
Class<?> clazz,
int depth
) throws InvocationTargetException, InstantiationException, IllegalAccessException, GenerationException {

if (!canBeGenerated(clazz)) {
throw new GenerationException(
"Class is not annotated with @" + Generatable.class.getSimpleName() + " and not a simple type"
);
}

// todo primitives cannot be null
if (depth > maxDepth) {
return null;
}

if (generators.containsKey(clazz)) {
return generators.get(clazz).get();
}

if (clazz.isEnum()) {
return generateEnum(clazz);
}

if (clazz.isArray()) {
return generateArray(clazz, depth);
}

if (Collection.class.isAssignableFrom(clazz)) {
return generateCollectionFromClass(clazz);
}

if (Map.class.isAssignableFrom(clazz)) {
return generateMapFromClass(clazz);
}

if (clazz.isInterface()) {
Class<?> implementationClass = findImplementationClass(clazz).orElseThrow(
() -> new GenerationException("No implementation found for interface " + clazz.getName())
);
return generateValueOfType(implementationClass, depth); // not incrementing depth on purpose
}

return generateCommonClass(clazz, depth);
}

private boolean canBeGenerated(Class<?> clazz) {
return generators.containsKey(clazz) ||
clazz.isEnum() ||
clazz.isArray() ||
Collection.class.isAssignableFrom(clazz) ||
Map.class.isAssignableFrom(clazz) ||
clazz.isAnnotationPresent(Generatable.class);
}

// todo вынести длину в параметр
private Object generateArray(
Class<?> arrayClass,
int depth
) throws GenerationException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<?> arrayElementClass = arrayClass.getComponentType();

if (arrayElementClass == null) {
throw new IllegalStateException("generateArray received not array as a parameter");
}

int length = random.nextInt(1, 10);
Object result = Array.newInstance(arrayElementClass, length);

for (int i = 0; i < length; ++i) {
Object element = generateValueOfType(arrayElementClass, depth + 1);
Array.set(result, i, element);
}

return result;
}

private Object generateEnum(Class<?> enumClass) throws GenerationException {
Object[] values = enumClass.getEnumConstants();

if (values == null) {
// should not happen
throw new IllegalStateException("generateEnum received not enum as a parameter");
}

if (0 == values.length) {
throw new GenerationException("enum '" + enumClass.getName() +
"' cannot generated, because values is empty"
);
}

return values[random.nextInt(values.length)];
}

private Collection<Object> generateCollectionFromClass(Class<?> collectionClass) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Напомните пж, почему мы тут не делаем также, как и в fromField варианте:
if (genericType instanceof ParameterizedType parameterizedType

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В рантайме тип generic'а стирается

Демонстрация:

Попробуем воспроизвести этот код в метода generateCollectionFromClass для Cart.java

        Type genericType = collectionField.getGenericType();
        Collection<Object> collection = generateCollectionFromClass(collectionField.getType());

        if (genericType instanceof ParameterizedType parameterizedType) {
            Type[] typeArgs = parameterizedType.getActualTypeArguments();

            // if not generic, then fill
            if (typeArgs.length == 1 && typeArgs[0] instanceof Class<?> elementType) {
                int length = random.nextInt(1, 10);

                for (int i = 0; i < length; ++i) {
                    Object element = generateValueOfType(elementType, depth + 1);
                    collection.add(element);
                }
            }
        }
  1. При использовании List instanceof ParameterizedType false
  2. Попробуем объявить поле items ArrayList -> false, пытаемся достать generic через getGenericSuperClass -> получаем E, с верхней границей Object. (можно достать getGenericInfo прямо у List, но результат будет аналогичный)
Image Image Image Image

Теперь посмотрим на Field

Класс знает, что тип поля не изменится и может сохранить значение generic'а у себя в метаинформации, на скрине видно, что эта информация легко доступна

Image Image

return switch (collectionClass) {
case Class<?> c when Set.class.isAssignableFrom(c) -> new HashSet<>();
case Class<?> c when Queue.class.isAssignableFrom(c) -> new LinkedList<>();
default -> new ArrayList<>();
};
}

private Map<Object, Object> generateMapFromClass(Class<?> mapClass) {
if (SortedMap.class.isAssignableFrom(mapClass)) {
return new TreeMap<>();
}
return new HashMap<>();
}

private Optional<Class<?>> findImplementationClass(Class<?> interfaceClass) {
if (!interfaceClass.getPackageName().startsWith(packageToScan)) {
return Optional.empty();
}

List<Class<?>> implementations = classesInPackageToScan.stream().filter(c ->
interfaceClass.isAssignableFrom(c) &&
c.isAnnotationPresent(Generatable.class) &&
!c.isInterface() &&
!Modifier.isAbstract(c.getModifiers())
).toList();
if (implementations.isEmpty()) {
return Optional.empty();
}
return Optional.of(implementations.get(random.nextInt(implementations.size())));
}

private Object generateCommonClass(
Class<?> clazz,
int depth
) throws GenerationException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
for (int i = 0; i < constructors.length; i++) {
Constructor<?> constructor = constructors[i];
try {
return tryConstructor(constructor, depth);
} catch (Exception e) {
if (i == constructors.length - 1) {
throw e;
}
}
}

int randomConstructorIndex = new Random().nextInt(constructors.length);
Constructor<?> randomConstructor = constructors[randomConstructorIndex];
return randomConstructor.newInstance(111);
throw new GenerationException("No suitable constructor found for class: " + clazz.getName());
}

private Object tryConstructor(
Constructor<?> constructor,
int depth
) throws GenerationException, InvocationTargetException, InstantiationException, IllegalAccessException {
Object[] paramValues = new Object[constructor.getParameterCount()];

for (int i = 0; i < constructor.getParameterCount(); i++) {
Class<?> paramType = constructor.getParameterTypes()[i];
paramValues[i] = generateValueOfType(paramType, depth + 1);
}

var instance = constructor.newInstance(paramValues);

var clazz = instance.getClass();

for (Field field : clazz.getDeclaredFields()) {
int modifiers = field.getModifiers();
if (Modifier.isStatic(modifiers) || Modifier.isFinal(modifiers)) {
continue;
}

field.setAccessible(true);
Class<?> fieldClass = field.getType();

Object fieldValue = switch (fieldClass) {
case Class<?> c when Collection.class.isAssignableFrom(c) -> generateCollectionFromField(field, depth);
case Class<?> c when Map.class.isAssignableFrom(c) -> generateMapFromField(field, depth);
default -> generateValueOfType(fieldClass, depth + 1);
};

field.set(instance, fieldValue);
}

return instance;
}

private Collection<?> generateCollectionFromField(
Field collectionField,
int depth
) throws GenerationException, InvocationTargetException, InstantiationException, IllegalAccessException {
Type genericType = collectionField.getGenericType();
Collection<Object> collection = generateCollectionFromClass(collectionField.getType());

if (genericType instanceof ParameterizedType parameterizedType) {
Type[] typeArgs = parameterizedType.getActualTypeArguments();

// if not generic, then fill
if (typeArgs.length == 1 && typeArgs[0] instanceof Class<?> elementType) {
int length = random.nextInt(1, 10);

for (int i = 0; i < length; ++i) {
Object element = generateValueOfType(elementType, depth + 1);
collection.add(element);
}
}
}

return collection;
}

private Map<?, ?> generateMapFromField(
Field mapField,
int depth
) throws GenerationException, InvocationTargetException, InstantiationException, IllegalAccessException {
Type genericType = mapField.getGenericType();
Map<Object, Object> map = generateMapFromClass(mapField.getType());

if (genericType instanceof ParameterizedType parameterizedType) {
Type[] typeArgs = parameterizedType.getActualTypeArguments();

if (typeArgs.length == 2 &&
typeArgs[0] instanceof Class<?> keyType &&
typeArgs[1] instanceof Class<?> valueType) {

int size = random.nextInt(1, 10);

for (int i = 0; i < size; i++) {
Object key = generateValueOfType(keyType, depth + 1);
Object value = generateValueOfType(valueType, depth + 1);
map.put(key, value);
}
}
}

return map;
}
}
Loading