Skip to content

Commit

Permalink
Merge pull request quarkusio#8116 from machi1990/refactor/review-serv…
Browse files Browse the repository at this point in the history
…ice-locators-scanning-for-liquibase-extension

Liquibase - Record service classes implementation instead of  generating a native resource file for each service interface
  • Loading branch information
gsmet authored Apr 14, 2020
2 parents 328e5d0 + 074168d commit 7894a22
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 71 deletions.
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

0 comments on commit 7894a22

Please sign in to comment.