Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

public class CodeLoadedChangeBuilder implements LoadedChangeBuilder<CodeLoadedChange> {

Expand Down Expand Up @@ -229,13 +232,31 @@ private void setRecoveryFromClass(Class<?> sourceClass) {
}
}

// Tried in order: the thread's context classloader is what frameworks like Spring Boot DevTools
// point at the "live" app classes (e.g. its RestartClassLoader), so it must win when present.
// This class's own loader is the pre-existing fallback, kept for environments (CLI, plain Java)
// where no context classloader is set. The system loader is a last resort.
private Class<?> getClassForName(String clazzName) {
try {
return Class.forName(clazzName);
}
catch (ClassNotFoundException e) {
throw new RuntimeException(e);
ClassNotFoundException lastException = null;
for (ClassLoader candidate : candidateClassLoaders()) {
if (candidate == null) {
continue;
}
try {
return Class.forName(clazzName, true, candidate);
} catch (ClassNotFoundException e) {
lastException = e;
}
}
throw new RuntimeException(lastException);
}

private Set<ClassLoader> candidateClassLoaders() {
return new LinkedHashSet<>(Arrays.asList(
Thread.currentThread().getContextClassLoader(),
getClass().getClassLoader(),
ClassLoader.getSystemClassLoader()
));
}

private Constructor<?> getConstructorFromPreview(CodePreviewChange preview) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import io.flamingock.internal.common.core.context.Context;
import io.flamingock.internal.common.core.context.Dependency;
import io.flamingock.internal.util.Property;
import io.flamingock.internal.util.log.FlamingockLoggerFactory;
import org.slf4j.Logger;

import java.io.File;
import java.net.InetAddress;
Expand Down Expand Up @@ -46,6 +48,8 @@

public class SimpleContext extends AbstractSimpleContextResolver implements Context {

private static final Logger logger = FlamingockLoggerFactory.getLogger("SimpleContext");

private final Map<String, Dependency> dependenciesByName;
private final Map<Class<?>, Dependency> dependenciesByExactType;

Expand All @@ -64,9 +68,23 @@ protected Optional<Dependency> getByType(Class<?> type) {
Optional<Dependency> dependencyByExactClass = Optional.ofNullable(dependenciesByExactType.get(type));
if (dependencyByExactClass.isPresent()) {
return dependencyByExactClass;
} else {
return getFirstAssignableDependency(type);
}
Optional<Dependency> assignableDependency = getFirstAssignableDependency(type);
if (assignableDependency.isPresent()) {
return assignableDependency;
}
// Fallback for types with the same fully-qualified name loaded by different classloaders
// (e.g. Spring Boot DevTools' restart classloader), where Class<?> identity/assignability
// checks above never match even though it's logically the same application type.
Optional<Dependency> sameNameDependency = getFirstSameNameDependency(type);
if (sameNameDependency.isPresent()) {
logger.warn("Dependency[{}] resolved by class name across a classloader boundary " +
"(requested type and registered type share the name but are different Class instances). " +
"This usually happens under a hot-reload classloader (e.g. Spring Boot DevTools). " +
"If this is unexpected, check for duplicate classes on the classpath.",
type.getName());
}
return sameNameDependency;
}

private Optional<Dependency> getFirstAssignableDependency(Class<?> type) {
Expand All @@ -76,6 +94,13 @@ private Optional<Dependency> getFirstAssignableDependency(Class<?> type) {
.findFirst();
}

private Optional<Dependency> getFirstSameNameDependency(Class<?> type) {
return dependenciesByExactType.entrySet().stream()
.filter(entry -> type.getName().equals(entry.getKey().getName()))
.map(Map.Entry::getValue)
.findFirst();
}

@Override
public void addDependency(Dependency dependency) {
if (!dependency.isDefaultNamed()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2023 Flamingock (https://www.flamingock.io)
*
* 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
*
* http://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 io.flamingock.internal.core.context;

import io.flamingock.internal.common.core.context.Dependency;
import org.junit.jupiter.api.Test;

import java.io.InputStream;
import java.net.URL;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Reproduces https://github.com/flamingock/flamingock-java/issues/951
* <p>
* Simulates what Spring Boot DevTools' RestartClassLoader does: the same
* fully-qualified class is loaded twice by two different classloaders,
* producing two distinct {@code Class<?>} instances with the same name.
* SimpleContext keys its dependency map by exact {@code Class<?>} identity,
* so a dependency registered under one loader's Class instance is not found
* when looked up with the other loader's Class instance.
*/
class ClassloaderMismatchReproTest {

private static final String TARGET_CLASS = "io.flamingock.internal.core.context.repro.MigrationConfiguration";

@Test
void dependencyRegisteredUnderOneClassloaderIsNotFoundUnderAnother() throws Exception {
ClassLoader appLoader = ClassloaderMismatchReproTest.class.getClassLoader();
Class<?> typeFromRegistration = loadIsolated(appLoader).loadClass(TARGET_CLASS);
Class<?> typeFromLookup = loadIsolated(appLoader).loadClass(TARGET_CLASS);

assertFalse(typeFromRegistration.equals(typeFromLookup),
"precondition: the two loaders must produce distinct Class instances");

SimpleContext context = new SimpleContext();
Object instance = typeFromRegistration.getConstructor(String.class)
.newInstance("some-config");
context.addDependency(new Dependency(typeFromRegistration, instance));

boolean found = context.getDependency(typeFromLookup).isPresent();

assertTrue(found, "dependency should be found by name across classloader boundaries");
}

private static IsolatedClassLoader loadIsolated(ClassLoader parent) {
return new IsolatedClassLoader(parent);
}

/** Loads only classes under the repro package in isolation; delegates everything else to parent. */
private static class IsolatedClassLoader extends ClassLoader {
private final ClassLoader resourceLoader;

IsolatedClassLoader(ClassLoader resourceLoader) {
super(null);
this.resourceLoader = resourceLoader;
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.equals(TARGET_CLASS)) {
synchronized (getClassLoadingLock(name)) {
Class<?> loaded = findLoadedClass(name);
if (loaded == null) {
String path = name.replace('.', '/') + ".class";
try (InputStream is = resourceLoader.getResourceAsStream(path)) {
if (is == null) {
throw new ClassNotFoundException(name);
}
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = is.read(buf)) != -1) {
baos.write(buf, 0, n);
}
byte[] bytes = baos.toByteArray();
loaded = defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
throw new ClassNotFoundException(name, e);
}
}
if (resolve) {
resolveClass(loaded);
}
return loaded;
}
}
return Class.forName(name, resolve, resourceLoader);
}

@Override
public URL getResource(String name) {
return resourceLoader.getResource(name);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2023 Flamingock (https://www.flamingock.io)
*
* 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
*
* http://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 io.flamingock.internal.core.context.repro;

public class MigrationConfiguration {
private final String configCollection;

public MigrationConfiguration(String configCollection) {
this.configCollection = configCollection;
}

public String getConfigCollection() {
return configCollection;
}
}
Loading