Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Liquibase - Record service classes implementation instead of generating a native resource file for each service interface #8116

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
@@ -1,15 +1,14 @@
package io.quarkus.liquibase;

import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;
import static io.quarkus.liquibase.runtime.graal.LiquibaseServiceLoader.serviceResourceFile;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -63,7 +62,9 @@ CapabilityBuildItem capability() {
}

@BuildStep(onlyIf = NativeBuild.class)
@Record(STATIC_INIT)
void nativeImageConfiguration(
LiquibaseRecorder recorder,
List<JdbcDataSourceBuildItem> jdbcDataSourceBuildItems,
BuildProducer<ReflectiveClassBuildItem> reflective,
BuildProducer<NativeImageResourceBuildItem> resource,
Expand Down Expand Up @@ -96,6 +97,8 @@ void nativeImageConfiguration(
liquibase.sql.visitor.SqlVisitor.class);

// load the liquibase services
Map<String, List<String>> serviceClassesImplementationRegistry = new HashMap<>();

Stream.of(liquibase.diff.compare.DatabaseObjectComparator.class,
liquibase.parser.NamespaceDetails.class,
liquibase.precondition.Precondition.class,
Expand All @@ -108,9 +111,11 @@ void nativeImageConfiguration(
liquibase.executor.Executor.class,
liquibase.lockservice.LockService.class,
liquibase.sqlgenerator.SqlGenerator.class)
.forEach(t -> addService(reflective, generatedResource, resource, t, true));
.forEach(t -> addService(reflective, t, true, serviceClassesImplementationRegistry));

addService(reflective, generatedResource, resource, liquibase.license.LicenseService.class, false);
addService(reflective, liquibase.license.LicenseService.class, false, serviceClassesImplementationRegistry);

recorder.setServicesImplementations(serviceClassesImplementationRegistry);

Collection<String> dataSourceNames = jdbcDataSourceBuildItems.stream()
.map(i -> i.getName())
Expand Down Expand Up @@ -179,28 +184,26 @@ ServiceStartBuildItem configureRuntimeProperties(LiquibaseRecorder recorder,
/**
* Search for all implementation of the interface {@code className}.
* <p>
* The service interface is added to the reflection configuration.
* Each implementation is added to the reflection configuration and added to a text file
* which contains all the implementation classes.
* This text file is added to the native resources and is used to load the implementations
* of the service.
* Each implementation is added to the reflection configuration and recorded
* in a map which used to load the implementations of the service in native image.
*/
private void addService(BuildProducer<ReflectiveClassBuildItem> reflective,
BuildProducer<GeneratedResourceBuildItem> generatedResourceProducer,
BuildProducer<NativeImageResourceBuildItem> resourceProducer,
Class<?> className, boolean methods) {

Class<?>[] impl = ServiceLocator.getInstance().findClasses(className);
if (impl != null && impl.length > 0) {
reflective.produce(new ReflectiveClassBuildItem(true, methods, false, impl));
String resourcesList = Arrays.stream(impl)
.map(Class::getName)
.collect(Collectors.joining("\n", "", "\n"));
String resourceName = serviceResourceFile(className);
generatedResourceProducer.produce(
new GeneratedResourceBuildItem(resourceName, resourcesList.getBytes(StandardCharsets.UTF_8)));
resourceProducer.produce(new NativeImageResourceBuildItem(resourceName));
private void addService(BuildProducer<ReflectiveClassBuildItem> reflective, Class<?> className, boolean methods,
Map<String, List<String>> serviceClassesImplementationRegistry) {

Class<?>[] classImplementations = ServiceLocator.getInstance().findClasses(className);

if (classImplementations != null && classImplementations.length > 0) {
reflective.produce(new ReflectiveClassBuildItem(true, methods, false, classImplementations));
List<String> serviceImplementations = new ArrayList<>();

for (Class<?> classImpl : classImplementations) {
serviceImplementations.add(classImpl.getName());
}

serviceClassesImplementationRegistry.put(className.getName(), serviceImplementations);
}

reflective.produce(new ReflectiveClassBuildItem(false, false, false, className.getName()));
reflective.produce(new ReflectiveClassBuildItem(false, false, false, className.getName()));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.quarkus.liquibase.runtime;

import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.enterprise.inject.Default;
Expand All @@ -10,6 +12,7 @@
import io.quarkus.arc.runtime.BeanContainerListener;
import io.quarkus.liquibase.LiquibaseDataSource;
import io.quarkus.liquibase.LiquibaseFactory;
import io.quarkus.liquibase.runtime.graal.LiquibaseServiceLoader;
import io.quarkus.runtime.annotations.Recorder;
import liquibase.Liquibase;
import liquibase.exception.LiquibaseException;
Expand All @@ -20,6 +23,10 @@
@Recorder
public class LiquibaseRecorder {

public void setServicesImplementations(Map<String, List<String>> serviceLoader) {
LiquibaseServiceLoader.setServicesImplementations(serviceLoader);
}

/**
* Sets the liquibase build configuration
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,67 +1,46 @@
package io.quarkus.liquibase.runtime.graal;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import org.jboss.logging.Logger;
import java.util.Map;

/**
* The liquibase service class loader for native image.
*
* The liquibase extension has its own implementation of the class-path scanner to load the Services (Interface implementation).
* For this we generate .txt files with the list of implementation classes at build time which are used in the native runtime.
*/
public class LiquibaseServiceLoader {

private static final Logger LOGGER = Logger.getLogger(LiquibaseServiceLoader.class);

/**
* File prefix with the service implementation classes list. It is generated dynamically in the Liquibase Quarkus Processor.
*/
private final static String SERVICES_IMPL = "META-INF/liquibase/";
private static Map<String, List<String>> servicesImplementations;

/**
* The service implementation classes list resource name
*
* @param requiredInterface the required interface
* @return the resource file in the class-path
*/
public static String serviceResourceFile(Class<?> requiredInterface) {
return SERVICES_IMPL + requiredInterface.getName() + ".txt";
}

/**
* Finds all classes for the required interface. The list of classes will be load from the txt files
* which are generated in the build phase.
* Finds all classes for the required interface.
*
* @param requiredInterface the required interface
* @return the list of the classes that implements the required interface
*/
public static List<Class<?>> findClassesImpl(Class<?> requiredInterface) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
String resourceName = serviceResourceFile(requiredInterface);
LOGGER.debugf("Liquibase service resource file: %s", resourceName);
try (InputStream resource = classLoader.getResourceAsStream(resourceName);
BufferedReader reader = new BufferedReader(
new InputStreamReader(Objects.requireNonNull(resource), StandardCharsets.UTF_8))) {
List<String> classesImplementationNames = servicesImplementations.get(requiredInterface.getName());

return reader.lines().map(className -> {
try {
LOGGER.debugf("Loading liquibase class: %s", className);
return Class.forName(className);
} catch (ClassNotFoundException ex) {
throw new IllegalStateException(ex);
}
}).collect(Collectors.toList());
} catch (IOException e) {
throw new IllegalStateException(e);
if (classesImplementationNames == null || classesImplementationNames.isEmpty()) {
return Collections.emptyList();
}

List<Class<?>> classImplementations = new ArrayList<>();

for (String classImplementation : classesImplementationNames) {
try {
classImplementations.add(Class.forName(classImplementation));
} catch (ClassNotFoundException exception) {
throw new IllegalStateException(exception);
}
}

return classImplementations;
}

public static void setServicesImplementations(Map<String, List<String>> servicesImplementations) {
LiquibaseServiceLoader.servicesImplementations = servicesImplementations;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
/**
* The liquibase service locator substitute replaces liquibase classpath scanner method
* {@link liquibase.servicelocator.ServiceLocator#findClasses(Class)} with a custom implementation
* {@link LiquibaseServiceLoader#findClassesImpl(Class)} which used the prebuilt txt file.
* {@link LiquibaseServiceLoader#findClassesImpl(Class)}.
*/
@TargetClass(className = "liquibase.servicelocator.ServiceLocator")
final class SubstituteServiceLocator {

@Substitute
private List<Class<?>> findClassesImpl(Class<?> requiredInterface) throws Exception {
private List<Class<?>> findClassesImpl(Class<?> requiredInterface) {
return LiquibaseServiceLoader.findClassesImpl(requiredInterface);
}

Expand Down