[#752][#658][#658] merged picocli-spring-boot-autoconfigure into picocli-spring-boot-starter

For now I prefer the simplicity of having a single module.
The [docs](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-developing-auto-configuration.html#boot-features-custom-starter-naming) mention that if you only have one module that combines [the starter and auto-configuration], name it XXX-spring-boot-starter.
This commit is contained in:
Remko Popma
2019-07-10 21:22:03 +09:00
parent eb5dd761b5
commit 06d4ad88f6
12 changed files with 8 additions and 105 deletions

View File

@@ -6,19 +6,20 @@ plugins {
}
group 'info.picocli'
description 'Picocli Spring Boot Starter - Dependency Descriptor to Easily get Started with Picocli and Spring.'
description 'Picocli Spring Boot Starter - Enables Spring Dependency Injection and Spring Boot AutoConfiguration in Picocli Commands.'
version "$projectVersion"
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile "info.picocli:picocli-spring-boot-autoconfigure:$projectVersion"
compile rootProject
compileOnly project(':picocli-codegen')
compile "org.springframework.boot:spring-boot-starter:$springBootVersion"
compileOnly "org.springframework.boot:spring-boot-configuration-processor:$springBootVersion"
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor:$springBootVersion"
}
jar {
@@ -29,7 +30,7 @@ jar {
'Implementation-Title' : 'Picocli Spring Boot Starter',
'Implementation-Vendor' : 'Remko Popma',
'Implementation-Version': version,
'Automatic-Module-Name' : 'info.picocli.spring.boot.starter'
'Automatic-Module-Name' : 'info.picocli.spring'
}
}
@@ -88,7 +89,7 @@ publishing {
root.appendNode('name', bintrayPackage)
root.appendNode('description', description)
root.appendNode('url', 'http://picocli.info')
root.appendNode('inceptionYear', '2018')
root.appendNode('inceptionYear', '2019')
root.children().last() + pomConfig
}
}

View File

@@ -0,0 +1,37 @@
package picocli.spring;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import picocli.CommandLine;
/**
* @author Thibaud Leprêtre
*/
public class PicocliSpringFactory implements CommandLine.IFactory {
private static final Logger logger = LoggerFactory.getLogger(PicocliSpringFactory.class);
private final ApplicationContext applicationContext;
public PicocliSpringFactory(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public <K> K create(Class<K> clazz) throws Exception {
try {
return getBeanOrCreate(clazz);
} catch (Exception e) {
logger.warn("Unable to get bean of class {}, using default Picocli factory", clazz);
return CommandLine.defaultFactory().create(clazz);
}
}
private <K> K getBeanOrCreate(Class<K> clazz) {
try {
return applicationContext.getBean(clazz);
} catch (Exception e) {
return applicationContext.getAutowireCapableBeanFactory().createBean(clazz);
}
}
}

View File

@@ -0,0 +1,32 @@
package picocli.spring.boot.autoconfigure;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import picocli.CommandLine;
import picocli.CommandLine.IFactory;
import picocli.spring.PicocliSpringFactory;
/**
* @author Thibaud Leprêtre
*/
@Configuration
@ConditionalOnClass(CommandLine.class)
public class PicocliAutoConfiguration {
@Primary
@Bean
@ConditionalOnMissingBean(IFactory.class)
public IFactory picocliSpringFactory(ApplicationContext applicationContext) {
return new PicocliSpringFactory(applicationContext);
}
@Bean
@ConditionalOnMissingBean(PicocliSpringFactory.class)
public PicocliSpringFactory picocliSpringFactoryImpl(ApplicationContext applicationContext) {
return new PicocliSpringFactory(applicationContext);
}
}

View File

@@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=picocli.spring.boot.autoconfigure.PicocliAutoConfiguration

View File

@@ -0,0 +1,86 @@
package picocli.spring;
import org.junit.After;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import picocli.CommandLine;
import picocli.CommandLine.IFactory;
import picocli.CommandLine.ParseResult;
import picocli.spring.boot.autoconfigure.sample.MyCommand;
import picocli.spring.boot.autoconfigure.sample.MySpringApp;
import java.util.Arrays;
import static org.junit.Assert.*;
public class PicocliSpringFactoryTest {
private AnnotationConfigApplicationContext context;
@After
public void tearDown() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void defaultPicocliSpringFactory() {
load(MySpringApp.class);
IFactory factory = this.context.getBean(IFactory.class);
assertNotNull(factory);
assertTrue(factory instanceof PicocliSpringFactory);
}
@Test
public void testParseTopLevelCommand() {
load(MySpringApp.class);
IFactory factory = this.context.getBean(IFactory.class);
MyCommand userObject = this.context.getBean(MyCommand.class);
CommandLine cmd = new CommandLine(userObject, factory);
cmd.parseArgs("-x", "abc", "xyz");
assertEquals("abc", userObject.x);
assertEquals(Arrays.asList("xyz"), userObject.positionals);
}
@Test
public void testParseSubCommand() {
load(MySpringApp.class);
IFactory factory = this.context.getBean(IFactory.class);
MyCommand userObject = this.context.getBean(MyCommand.class);
CommandLine cmd = new CommandLine(userObject, factory);
ParseResult parseResult = cmd.parseArgs("sub", "-y", "abc", "xyz");
assertNull(userObject.x);
assertNull(userObject.positionals);
assertTrue(parseResult.hasSubcommand());
MyCommand.Sub sub = (MyCommand.Sub) parseResult.subcommand().commandSpec().userObject();
assertEquals("abc", sub.y);
assertEquals(Arrays.asList("xyz"), sub.positionals);
}
@Test
public void testParseSubSubCommand() {
load(MySpringApp.class);
IFactory factory = this.context.getBean(IFactory.class);
MyCommand userObject = this.context.getBean(MyCommand.class);
CommandLine cmd = new CommandLine(userObject, factory);
ParseResult parseResult = cmd.parseArgs("sub", "subsub", "-z", "abc");
assertNull(userObject.x);
assertNull(userObject.positionals);
assertTrue(parseResult.hasSubcommand());
assertTrue(parseResult.subcommand().hasSubcommand());
MyCommand.SubSub subsub = (MyCommand.SubSub) parseResult.subcommand().subcommand().commandSpec().userObject();
assertEquals("abc", subsub.z);
assertEquals("something", subsub.service.service());
}
private void load(Class<?> config, String... environment) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
//EnvironmentTestUtils.addEnvironment(applicationContext, environment);
applicationContext.register(config);
applicationContext.refresh();
this.context = applicationContext;
}
}

View File

@@ -0,0 +1,119 @@
package picocli.spring.boot.autoconfigure;
import org.junit.After;
import org.junit.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.IFactory;
import picocli.spring.PicocliSpringFactory;
import static org.junit.Assert.*;
public class PicocliAutoConfigurationTest {
private AnnotationConfigApplicationContext context;
@After
public void tearDown() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void defaultPicocliSpringFactory() {
load(EmptyConfiguration.class);
IFactory factory = this.context.getBean(PicocliSpringFactory.class);
assertNotNull(factory);
assertTrue(factory instanceof PicocliSpringFactory);
CommandLine cmd = new CommandLine(new MyCommand(), factory);
cmd.parseArgs();
}
@Test
public void defaultFactory() {
load(EmptyConfiguration.class);
IFactory factory = this.context.getBean(IFactory.class);
assertNotNull(factory);
assertTrue(factory instanceof PicocliSpringFactory);
}
@Test
public void testEnableAutoConfigurationRequired() {
load(EmptyNoAutoConfiguration.class);
try {
this.context.getBean(IFactory.class);
fail("Expected exception");
} catch (NoSuchBeanDefinitionException ok) {
assertEquals("No qualifying bean of type 'picocli.CommandLine$IFactory' available", ok.getMessage());
}
}
@Test
public void testEnableAutoConfigurationImplRequired() {
load(EmptyNoAutoConfiguration.class);
try {
this.context.getBean(PicocliSpringFactory.class);
fail("Expected exception");
} catch (NoSuchBeanDefinitionException ok) {
assertEquals("No qualifying bean of type 'picocli.spring.PicocliSpringFactory' available", ok.getMessage());
}
}
@Test
public void configuredFactory() {
load(CommandIFactoryConfiguration.class);
IFactory factory = this.context.getBean(IFactory.class);
assertNotNull(factory);
assertTrue(factory instanceof PicocliSpringFactory);
}
@Test
public void configuredFactoryImpl() {
load(CommandPicocliSpringFactoryConfiguration.class);
IFactory factory = this.context.getBean(IFactory.class);
assertNotNull(factory);
assertTrue(factory instanceof PicocliSpringFactory);
}
@Configuration
@EnableAutoConfiguration
static class EmptyConfiguration {}
@Configuration
static class EmptyNoAutoConfiguration {}
@Configuration
@EnableAutoConfiguration
static class CommandIFactoryConfiguration {
@Autowired
IFactory factory;
}
@Configuration
@EnableAutoConfiguration
static class CommandPicocliSpringFactoryConfiguration {
@Autowired
PicocliSpringFactory factory;
}
@Component
@Command
static class MyCommand {}
private void load(Class<?> config, String... environment) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
//EnvironmentTestUtils.addEnvironment(applicationContext, environment);
applicationContext.register(config);
applicationContext.refresh();
this.context = applicationContext;
}
}

View File

@@ -0,0 +1,64 @@
package picocli.spring.boot.autoconfigure.sample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.util.List;
import java.util.concurrent.Callable;
// NOTE: inner classes and fields are public for testing
@Component
@Command(name = "mycommand", mixinStandardHelpOptions = true, subcommands = MyCommand.Sub.class)
public class MyCommand implements Callable<Integer> {
@Option(names = "-x", description = "optional option")
public String x;
@Parameters(description = "positional params")
public List<String> positionals;
@Override
public Integer call() {
System.out.printf("mycommand was called with -x=%s and positionals: %s%n", x, positionals);
return 23;
}
@Component
@Command(name = "sub", mixinStandardHelpOptions = true, subcommands = MyCommand.SubSub.class,
exitCodeOnExecutionException = 34)
public static class Sub implements Callable<Integer> {
@Option(names = "-y", description = "optional option")
public String y;
@Parameters(description = "positional params")
public List<String> positionals;
@Override
public Integer call() {
System.out.printf("mycommand sub was called with -y=%s and positionals: %s%n", y, positionals);
throw new RuntimeException("mycommand sub failing on purpose");
//return 33;
}
}
@Component
@Command(name = "subsub", mixinStandardHelpOptions = true,
exitCodeOnExecutionException = 44)
public static class SubSub implements Callable<Integer> {
@Option(names = "-z", description = "optional option")
public String z;
@Autowired
public SomeService service;
@Override
public Integer call() {
System.out.printf("mycommand sub subsub was called with -z=%s. Service says: '%s'%n", z, service.service());
throw new RuntimeException("mycommand sub subsub failing on purpose");
//return 43;
}
}
}

View File

@@ -0,0 +1,49 @@
package picocli.spring.boot.autoconfigure.sample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import picocli.CommandLine;
import picocli.CommandLine.IFactory;
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class MySpringApp implements CommandLineRunner, ExitCodeGenerator {
private int exitCode;
@Autowired
IFactory factory;
@Autowired
MyCommand myCommand;
@Bean
ServiceDependency dependency() {
return new ServiceDependency();
}
@Bean
SomeService someService(ServiceDependency dependency) {
return new SomeService(dependency);
}
@Override
public void run(String... args) {
exitCode = new CommandLine(myCommand, factory).execute(args);
}
@Override
public int getExitCode() {
return exitCode;
}
public static void main(String[] args) {
System.exit(SpringApplication.exit(SpringApplication.run(MySpringApp.class, args)));
}
}

View File

@@ -0,0 +1,7 @@
package picocli.spring.boot.autoconfigure.sample;
public class ServiceDependency {
public String provideSomething() {
return "something";
}
}

View File

@@ -0,0 +1,13 @@
package picocli.spring.boot.autoconfigure.sample;
public class SomeService {
private final ServiceDependency dependency;
public SomeService(ServiceDependency dependency) {
this.dependency = dependency;
}
public String service() {
return dependency.provideSomething();
}
}