diff --git a/README.md b/README.md index 5e5a6ce..9b41d74 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ [![Travis](https://img.shields.io/travis/kakawait/picocli-spring-boot-starter.svg)](https://travis-ci.org/kakawait/picocli-spring-boot-starter) [![SonarQube Coverage](https://img.shields.io/sonar/https/sonarcloud.io/com.kakawait%3Apicocli-spring-boot-starter-parent/coverage.svg)](https://sonarcloud.io/component_measures?id=com.kakawait%3Apicocli-spring-boot-starter-parent&metric=coverage) -[![Maven Central](https://img.shields.io/maven-central/v/com.kakawait/picocli-spring-boot-starter.svg)](https://search.maven.org/#artifactdetails%7Ccom.kakawait%7Cpicocli-spring-boot-starter%7C0.2.0%7Cjar) +[![Maven Central](https://img.shields.io/maven-central/v/com.kakawait/picocli-spring-boot-starter.svg)](https://search.maven.org/#artifactdetails%7Ccom.kakawait%7Cpicocli-spring-boot-starter%7C1.0.0-beta-1%7Cjar) [![license](https://img.shields.io/github/license/kakawait/picocli-spring-boot-starter.svg)](https://github.com/kakawait/picocli-spring-boot-starter/blob/master/LICENSE.md) +[![Twitter Follow](https://img.shields.io/twitter/follow/thibaudlepretre.svg?style=social&label=%40thibaudlepretre)](https://twitter.com/intent/follow?screen_name=thibaudlepretre) > A Spring boot starter for [Picocli](http://picocli.info/) command line tools. That let you easily write CLI for Spring boot application! @@ -24,7 +25,7 @@ Add the Spring boot starter to your project com.kakawait picocli-spring-boot-starter - 0.2.0 + 1.0.0-beta-1 ``` @@ -116,9 +117,66 @@ But the following example will not be candidate for _Main_ command class MainCommand {} ``` -#### Nested sub-commands using beans +#### Nested sub-commands -Picocli allows [_nested sub-commands_](http://picocli.info/#_nested_sub_subcommands), in order to describe a _nested sub-command_, starter is offering you nested classes scanning capability. +Picocli allows [_nested sub-commands_](http://picocli.info/#_nested_sub_subcommands), in order to describe a _nested sub-command_, starter is offering two ways to describe your structure. + +Please refer to next points to see how to construct this following command line application using both way: + +``` +Commands: + flyway [-h, --help] + migrate + repair +``` + +`java -jar .jar flyway migrate` will execute _Flyway_ migration. + +You can mix methods but is not recommended! + +##### Using `subCommands` from `@Command` annotation + +```java +@Component +@Command(name = "flyway", subCommands = { MigrateCommand.class, RepairCommand.class }) +class FlywayCommand extends HelpAwareContainerPicocliCommand {} + +@Component +@Command(name = "migrate") +class MigrateCommand implements Runnable { + + private final Flyway flyway; + + public MigrateCommand(Flyway flyway) { + this.flyway = flyway; + } + + @Override + public void run() { + flyway.migrate(); + } +} + +@Component +@Command(name = "repair") +class RepairCommand implements Runnable { + private final Flyway flyway; + + public RepairCommand(Flyway flyway) { + this.flyway = flyway; + } + + @Override + public void run() { + flyway.repair(); + } +} +``` + +By default starter is providing a custom implementation [`ApplicationContextAwarePicocliFactory`](picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/ApplicationContextAwarePicocliFactory.java) of [`CommandLine.IFactory`](https://picocli.info/apidocs/picocli/CommandLine.IFactory.html) that will delegate instance creation to _Spring_ `ApplicationContext` in order to load bean if exists. +**ATTENTION** If subCommand is not a defined bean, [`ApplicationContextAwarePicocliFactory`](picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/ApplicationContextAwarePicocliFactory.java) will only instanciate class without any _autowiring_ capability. + +##### Using java nested class hierarchy That means, if you're defining **bean** structure like following: @@ -160,20 +218,9 @@ class FlywayCommand extends HelpAwareContainerPicocliCommand { } ``` -Will generate command line - -``` -Commands: - flyway [-h, --help] - migrate - repair -``` - -Thus `java -jar .jar flyway migrate` will execute _Flyway_ migration. - **ATTENTION** every classes must be a bean (`@Component`) with `@Command` annotation without forgetting to file `name` attribute. -There is **no limitation** about nesting level. +Otherwise, there is **no limitation** about nesting level. ### Additional configuration diff --git a/picocli-spring-boot-autoconfigure/pom.xml b/picocli-spring-boot-autoconfigure/pom.xml index 4393c77..7d83500 100644 --- a/picocli-spring-boot-autoconfigure/pom.xml +++ b/picocli-spring-boot-autoconfigure/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.kakawait picocli-spring-boot-autoconfigure - 0.2.0 + 1.0.0-beta-1 jar Picocli spring boot autoconfigure @@ -41,17 +41,17 @@ 1.8 1.8 - 1.5.4.RELEASE + 2.1.4.RELEASE - 0.9.8 + 3.9.6 1.7.25 - 3.8.0 + 3.12.2 2.0.0.0 - 2.15.0 + 2.27.0 3.0.1 - 2.10.4 + 3.0.1 1.6 1.6.8 @@ -81,7 +81,6 @@ org.slf4j slf4j-api - ${slf4j-api.version} @@ -95,7 +94,16 @@ junit test - + + ch.qos.logback + logback-classic + test + + + ch.qos.logback + logback-core + test + org.assertj assertj-core @@ -142,7 +150,7 @@ release - gpg2 + gpg diff --git a/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/ApplicationContextAwarePicocliFactory.java b/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/ApplicationContextAwarePicocliFactory.java new file mode 100644 index 0000000..c5e8a9b --- /dev/null +++ b/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/ApplicationContextAwarePicocliFactory.java @@ -0,0 +1,37 @@ +package com.kakawait.spring.boot.picocli.autoconfigure; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; +import picocli.CommandLine; + +import java.lang.reflect.Constructor; + +/** + * @author Thibaud Leprêtre + */ +public class ApplicationContextAwarePicocliFactory implements CommandLine.IFactory { + private static final Logger logger = LoggerFactory.getLogger(ApplicationContextAwarePicocliFactory.class); + + private final ApplicationContext applicationContext; + + public ApplicationContextAwarePicocliFactory(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public K create(Class aClass) throws Exception { + try { + return applicationContext.getBean(aClass); + } catch (Exception e) { + logger.info("unable to get bean of class {}, use standard factory creation", aClass); + try { + return aClass.newInstance(); + } catch (Exception ex) { + Constructor constructor = aClass.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } + } + } +} diff --git a/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliAutoConfiguration.java b/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliAutoConfiguration.java index aa2ff17..578c27e 100644 --- a/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliAutoConfiguration.java +++ b/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliAutoConfiguration.java @@ -1,10 +1,20 @@ package com.kakawait.spring.boot.picocli.autoconfigure; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.aop.support.AopUtils; import org.springframework.boot.CommandLineRunner; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -20,18 +30,9 @@ import org.springframework.context.annotation.Import; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.util.ReflectionUtils; -import picocli.CommandLine; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import static picocli.CommandLine.Command; +import picocli.CommandLine; +import picocli.CommandLine.Command; /** * @author Thibaud Leprêtre @@ -41,6 +42,12 @@ @Import(PicocliAutoConfiguration.CommandlineConfiguration.class) class PicocliAutoConfiguration { + @Bean + @ConditionalOnMissingBean(CommandLine.IFactory.class) + CommandLine.IFactory applicationAwarePicocliFactory(ApplicationContext applicationContext) { + return new ApplicationContextAwarePicocliFactory(applicationContext); + } + @Bean @ConditionalOnMissingBean(PicocliCommandLineRunner.class) @ConditionalOnBean(CommandLine.class) @@ -52,7 +59,13 @@ CommandLineRunner picocliCommandLineRunner(CommandLine cli) { @Conditional(CommandCondition.class) static class CommandlineConfiguration { - private final Logger logger = LoggerFactory.getLogger(CommandlineConfiguration.class); + private static final Logger logger = LoggerFactory.getLogger(CommandlineConfiguration.class); + + private final CommandLine.IFactory applicationAwarePicocliFactory; + + public CommandlineConfiguration(CommandLine.IFactory applicationAwarePicocliFactory) { + this.applicationAwarePicocliFactory = applicationAwarePicocliFactory; + } @Bean CommandLine picocliCommandLine(ApplicationContext applicationContext) { @@ -60,11 +73,11 @@ CommandLine picocliCommandLine(ApplicationContext applicationContext) { List mainCommands = getMainCommands(commands); Object mainCommand = mainCommands.isEmpty() ? new HelpAwarePicocliCommand() {} : mainCommands.get(0); if (mainCommands.size() > 1) { - logger.warn("Multiple mains command founds [{}], selected first one {}", mainCommands, mainCommand); + throw new RuntimeException("Multiple mains command founds: " + Collections.singletonList(mainCommands)); } commands.removeAll(mainCommands); - CommandLine cli = new CommandLine(mainCommand); + CommandLine cli = new CommandLine(mainCommand, applicationAwarePicocliFactory); registerCommands(cli, commands); applicationContext.getBeansOfType(PicocliConfigurer.class).values().forEach(c -> c.configure(cli)); @@ -159,13 +172,26 @@ private void registerCommands(CommandLine cli, Collection commands) { } else if (node.getParent() == null) { current = cli; } + if (children.isEmpty()) { - current.addSubcommand(commandName, command); + if (!current.getSubcommands().containsKey(commandName)) { + current.addSubcommand(commandName, command); + } } else { - CommandLine sub = new CommandLine(command); - current.addSubcommand(commandName, sub); + CommandLine sub; + if (!current.getSubcommands().containsKey(commandName)) { + sub = new CommandLine(command, applicationAwarePicocliFactory); + current.addSubcommand(commandName, sub); + } else { + // get the reference of subCommands from current, instead of creating new one + sub = current.getSubcommands().get(commandName); + } + for (Object child : children) { - sub.addSubcommand(getCommandName(child), new CommandLine(child)); + String childCommandName = getCommandName(child); + if (!sub.getSubcommands().containsKey(childCommandName)) { + sub.addSubcommand(childCommandName, new CommandLine(child, applicationAwarePicocliFactory)); + } } current = sub; } @@ -205,13 +231,26 @@ public boolean equals(Object o) { Node node = (Node) o; - return clazz != null ? clazz.equals(node.clazz) : node.clazz == null; + return Objects.equals(clazz, node.clazz); } @Override public int hashCode() { return clazz != null ? clazz.hashCode() : 0; } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Node [clazz="); + builder.append(clazz); + builder.append(", object="); + builder.append(object); + builder.append(", parent="); + builder.append(parent); + builder.append("]"); + return builder.toString(); + } } } diff --git a/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliCommandLineRunner.java b/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliCommandLineRunner.java index 82c8ddb..bf1c882 100644 --- a/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliCommandLineRunner.java +++ b/picocli-spring-boot-autoconfigure/src/main/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliCommandLineRunner.java @@ -39,7 +39,7 @@ public void run(String... args) throws Exception { cli.usage(System.err, Ansi.AUTO); return; } - if (isHelpRequested(cli.getCommand())) { + if (isHelpRequested((Object) cli.getCommand())) { cli.usage(System.out, Ansi.AUTO); return; } diff --git a/picocli-spring-boot-autoconfigure/src/test/java/com.kakawait.spring.boot.picocli.autoconfigure/PicocliAutoConfigurationTest.java b/picocli-spring-boot-autoconfigure/src/test/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliAutoConfigurationTest.java similarity index 64% rename from picocli-spring-boot-autoconfigure/src/test/java/com.kakawait.spring.boot.picocli.autoconfigure/PicocliAutoConfigurationTest.java rename to picocli-spring-boot-autoconfigure/src/test/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliAutoConfigurationTest.java index 30c9a9a..0d4d28e 100644 --- a/picocli-spring-boot-autoconfigure/src/test/java/com.kakawait.spring.boot.picocli.autoconfigure/PicocliAutoConfigurationTest.java +++ b/picocli-spring-boot-autoconfigure/src/test/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliAutoConfigurationTest.java @@ -1,25 +1,27 @@ package com.kakawait.spring.boot.picocli.autoconfigure; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.hamcrest.Matchers.matchesPattern; + +import java.util.Collection; +import java.util.function.Function; +import java.util.regex.Pattern; + import org.assertj.core.api.Condition; -import org.assertj.core.api.iterable.Extractor; import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.rule.OutputCapture; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; -import picocli.CommandLine; - -import java.util.Collection; -import java.util.regex.Pattern; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.hamcrest.Matchers.matchesPattern; -import static picocli.CommandLine.Command; +import picocli.CommandLine; +import picocli.CommandLine.Command; /** * @author Thibaud Leprêtre @@ -58,7 +60,8 @@ public void autoConfiguration_MissingMainCommand_ConfiguresDefaultHelpAwareComma load(SimpleConfiguration.class); PicocliCommandLineRunner runner = context.getBean(PicocliCommandLineRunner.class); - assertThat(runner.getCommandLine().getCommand()).isInstanceOf(HelpAwarePicocliCommand.class); + Object command = runner.getCommandLine().getCommand(); + assertThat(command).isInstanceOf(HelpAwarePicocliCommand.class); runner.run("-h"); @@ -74,7 +77,7 @@ public void autoConfiguration_BasicBeanDefinition_CreateSubCommands() { assertThat(runner.getCommandLine().getSubcommands().values()) .hasSameSizeAs(commands) - .extracting("interpreter.command") + .extractingResultOf("getCommand") .containsExactlyElementsOf(commands) .doNotHave(new Condition<>(SimpleConfiguration.NoBeanCommand.class::isInstance, "NoBeanCommand")); } @@ -85,9 +88,12 @@ public void autoConfiguration_NestedBeanDefinition_CreateNestedSubCommands() { PicocliCommandLineRunner runner = context.getBean(PicocliCommandLineRunner.class); Collection commands = context.getBeansWithAnnotation(Command.class).values(); - Extractor> extractor = input -> input.getSubcommands().values(); - + Function> extractor = input -> input.getSubcommands().values(); assertThat(commands).hasSize(5); + + CommandLine current = runner.getCommandLine(); + this.logCommandTree(current); + assertThat(runner.getCommandLine().getSubcommands().values()) .hasSize(1) .haveExactly(1, new Condition<>(e -> { @@ -116,16 +122,18 @@ public void autoConfiguration_NestedBeanDefinition_CreateNestedSubCommands() { }, "Class Level2Command")); } - @Test + private void logCommandTree(CommandLine current) { + for (String command : current.getSubcommands().keySet()) { + CommandLine subCommand = current.getSubcommands().get(command); + System.out.println( + "current command: " + current.getCommandName() + " sub command: " + subCommand.getCommandName()); + this.logCommandTree(subCommand); + } + } + + @Test(expected = RuntimeException.class) public void autoConfiguration_MultipleMainCommands_RandomUses() { load(MainCommandsConflictConfiguration.class); - PicocliCommandLineRunner runner = context.getBean(PicocliCommandLineRunner.class); - - assertThat(runner.getCommandLine()) - .is(new Condition<>( - c -> c.getCommand() instanceof MainCommandsConflictConfiguration.MainCommand2, - "Class MainCommand2")); - assertThat(runner.getCommandLine().getSubcommands()).hasSize(0); } @Test @@ -145,6 +153,30 @@ public void autoConfiguration_WithMultiplePicocliConfigurerAdapters_ApplyAll() { assertThat(runner.getCommandLine().getSubcommands()).containsKeys("¯\\_(ツ)_/¯"); } + @Test + public void autoConfiguration_WithSubCommandsParameter_GetBeanIfExists() { + load(SubCommandConfiguration.class); + PicocliCommandLineRunner runner = context.getBean(PicocliCommandLineRunner.class); + + assertThat(runner.getCommandLine().getSubcommands().get("basic").getSubcommands().values()) + .isNotEmpty() + .extracting(CommandLine::getCommand) + .allMatch(c -> c instanceof SubCommandConfiguration.DummyBeanCommand) + .allMatch(c -> ((SubCommandConfiguration.DummyBeanCommand) c).getDummyBean() != null); + } + + @Test + public void autoConfiguration_WithSubCommandsParameterAndNested_OnlyOneCommand() { + load(SubSubCommandConfiguration.class); + PicocliCommandLineRunner runner = context.getBean(PicocliCommandLineRunner.class); + + assertThat(runner.getCommandLine().getSubcommands().get("basic").getSubcommands().values()) + .hasSize(1) + .extracting(CommandLine::getCommand) + .allMatch(c -> c instanceof SubSubCommandConfiguration.BasicCommand.NestedSubCommand) + .allMatch(c -> ((SubSubCommandConfiguration.BasicCommand.NestedSubCommand) c).getDummyBean() != null); + } + @Configuration static class EmptyConfiguration { } @@ -243,6 +275,109 @@ public void configure(CommandLine commandLine) { static class BasicCommand {} } + @Configuration + static class SubCommandConfiguration { + @Bean + DummyBean dummyBean() { + return new DummyBean("¯\\_(ツ)_/¯"); + } + + static class DummyBean { + private String title; + + public DummyBean(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } + } + + @Component + @Command(name = "basic", + subcommands = { ConstructorInjectionSubCommand.class, SetterInjectionSubCommand.class }) + static class BasicCommand { + + @Component + @Command(name = "sub3") + static class NestedSubCommand implements DummyBeanCommand { + @Autowired + private DummyBean dummyBean; + + public DummyBean getDummyBean() { + return dummyBean; + } + } + } + + interface DummyBeanCommand { + DummyBean getDummyBean(); + } + + @Component + @Command(name = "sub1") + static class ConstructorInjectionSubCommand implements DummyBeanCommand { + private final DummyBean dummyBean; + + public ConstructorInjectionSubCommand(DummyBean dummyBean) { + this.dummyBean = dummyBean; + } + + public DummyBean getDummyBean() { + return dummyBean; + } + } + + @Component + @Command(name = "sub2") + static class SetterInjectionSubCommand implements DummyBeanCommand { + @Autowired + private DummyBean dummyBean; + + public DummyBean getDummyBean() { + return dummyBean; + } + } + } + + @Configuration + static class SubSubCommandConfiguration { + @Bean + DummyBean dummyBean() { + return new DummyBean("¯\\_(ツ)_/¯"); + } + + static class DummyBean { + private String title; + + public DummyBean(String title) { + this.title = title; + } + + public String getTitle() { + return title; + } + } + + @Component + @Command(name = "basic", + subcommands = { BasicCommand.NestedSubCommand.class }) + static class BasicCommand { + + @Component + @Command(name = "sub3") + static class NestedSubCommand { + @Autowired + private DummyBean dummyBean; + + public DummyBean getDummyBean() { + return dummyBean; + } + } + } + } + private void load(Class... configs) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(configs); diff --git a/picocli-spring-boot-autoconfigure/src/test/java/com.kakawait.spring.boot.picocli.autoconfigure/PicocliCommandLineRunnerTest.java b/picocli-spring-boot-autoconfigure/src/test/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliCommandLineRunnerTest.java similarity index 89% rename from picocli-spring-boot-autoconfigure/src/test/java/com.kakawait.spring.boot.picocli.autoconfigure/PicocliCommandLineRunnerTest.java rename to picocli-spring-boot-autoconfigure/src/test/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliCommandLineRunnerTest.java index 0fa0568..b971886 100644 --- a/picocli-spring-boot-autoconfigure/src/test/java/com.kakawait.spring.boot.picocli.autoconfigure/PicocliCommandLineRunnerTest.java +++ b/picocli-spring-boot-autoconfigure/src/test/java/com/kakawait/spring/boot/picocli/autoconfigure/PicocliCommandLineRunnerTest.java @@ -2,6 +2,8 @@ import net.bytebuddy.ByteBuddy; import net.bytebuddy.description.annotation.AnnotationDescription; +import net.bytebuddy.dynamic.loading.ClassInjector; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy; import org.junit.Before; import org.junit.Rule; @@ -14,7 +16,9 @@ import picocli.CommandLine; import picocli.CommandLine.Command; +import java.lang.invoke.MethodHandles; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -57,7 +61,7 @@ public void setup() { @Test public void run_ExceptionDuringParsing_PrintUsageAndStop() throws Exception { - when(cli.parse(any())).thenThrow(new ParameterException("Error when parsing")); + when(cli.parse(any())).thenThrow(new ParameterException(cli, "Error when parsing")); runner.run("parsing error or something else"); @@ -312,12 +316,36 @@ private PicocliCommand makePicocliCommand(String commandName, Callable callab } private Class make(String commandName, Class type) { - return new ByteBuddy() - .subclass(type, ConstructorStrategy.Default.IMITATE_SUPER_CLASS) - .annotateType(getCommandAnnotationDescription(commandName)) - .make() - .load(getClass().getClassLoader()) - .getLoaded(); + try { + return new ByteBuddy() + .subclass(type, ConstructorStrategy.Default.IMITATE_SUPER_CLASS) + .annotateType(getCommandAnnotationDescription(commandName)) + .make() + .load(getClass().getClassLoader(), getClassLoadingStrategy(type)) + .getLoaded(); + } catch (Exception e) { + throw new RuntimeException("Unable to generate dynamic class", e); + } + } + + private ClassLoadingStrategy getClassLoadingStrategy(Class targetClass) + throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + ClassLoadingStrategy strategy; + if (ClassInjector.UsingLookup.isAvailable()) { + Class methodHandles = Class.forName("java.lang.invoke.MethodHandles"); + Object lookup = methodHandles.getMethod("lookup").invoke(null); + Method privateLookupIn = methodHandles.getMethod("privateLookupIn", + Class.class, + Class.forName("java.lang.invoke.MethodHandles$Lookup")); + Object privateLookup = privateLookupIn.invoke(null, targetClass, lookup); + strategy = ClassLoadingStrategy.UsingLookup.of(privateLookup); + } else if (ClassInjector.UsingReflection.isAvailable()) { + strategy = ClassLoadingStrategy.Default.INJECTION; + } else { + throw new IllegalStateException("No code generation strategy available"); + } + + return strategy; } private static class DelegateRunnable implements Runnable { diff --git a/picocli-spring-boot-sample/pom.xml b/picocli-spring-boot-sample/pom.xml index 6b21ae8..e96c766 100644 --- a/picocli-spring-boot-sample/pom.xml +++ b/picocli-spring-boot-sample/pom.xml @@ -6,7 +6,7 @@ com.kakawait picocli-spring-boot-sample - 0.2.0 + 1.0.0-beta-1 UTF-8 @@ -15,15 +15,23 @@ 1.8 1.8 1.8 - 22.0 + + 2.1.4.RELEASE + + 27.1-jre - - org.springframework.boot - spring-boot-starter-parent - 1.5.4.RELEASE - - + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + @@ -48,8 +56,6 @@ guava ${guava.version} - - org.flywaydb flyway-core diff --git a/picocli-spring-boot-sample/src/main/java/com/kakawait/PicocliSpringBootSampleApplication.java b/picocli-spring-boot-sample/src/main/java/com/kakawait/PicocliSpringBootSampleApplication.java index a53754e..5bb70c5 100644 --- a/picocli-spring-boot-sample/src/main/java/com/kakawait/PicocliSpringBootSampleApplication.java +++ b/picocli-spring-boot-sample/src/main/java/com/kakawait/PicocliSpringBootSampleApplication.java @@ -182,7 +182,7 @@ public void run() { } @Component - @Command(name = "flyway") + @Command(name = "flyway", subcommands = { RepairCommand.class }) static class FlywayCommand extends HelpAwareContainerPicocliCommand { @Component @@ -200,20 +200,20 @@ public void run() { flyway.migrate(); } } + } - @Component - @Command(name = "repair") - static class RepairCommand implements Runnable { - private final Flyway flyway; + @Component + @Command(name = "repair") + static class RepairCommand implements Runnable { + private final Flyway flyway; - public RepairCommand(Flyway flyway) { - this.flyway = flyway; - } + public RepairCommand(Flyway flyway) { + this.flyway = flyway; + } - @Override - public void run() { - flyway.repair(); - } + @Override + public void run() { + flyway.repair(); } } diff --git a/picocli-spring-boot-sample/src/main/resources/application.yml b/picocli-spring-boot-sample/src/main/resources/application.yml index 49eae3f..7fd981f 100644 --- a/picocli-spring-boot-sample/src/main/resources/application.yml +++ b/picocli-spring-boot-sample/src/main/resources/application.yml @@ -1,6 +1,6 @@ spring: main: - web-environment: false + web-application-type: none logging: level: ROOT: off diff --git a/picocli-spring-boot-starter/pom.xml b/picocli-spring-boot-starter/pom.xml index cc93b1d..e8c79b0 100644 --- a/picocli-spring-boot-starter/pom.xml +++ b/picocli-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.kakawait picocli-spring-boot-starter - 0.2.0 + 1.0.0-beta-1 jar Picocli spring boot starter @@ -41,6 +41,8 @@ 1.8 1.8 + 2.1.4.RELEASE + 3.0.1 2.10.4 1.6 @@ -48,11 +50,11 @@ - + org.springframework.boot spring-boot-dependencies - 1.5.4.RELEASE + ${spring-boot.version} pom import @@ -90,7 +92,7 @@ release - gpg2 + gpg diff --git a/pom.xml b/pom.xml index 7e4fe61..732ce2c 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.kakawait picocli-spring-boot-starter-parent pom - 0.2.0 + 1.0.0-beta-1 Picocli spring boot starter parent Spring boot starter for Picocli command line parser that will simplify your CommandLineRunner