首先 , 传送门 spring-shell-java
由于某一些原因, 需要自己编写一个运行程序, 实现在命令行中使用命令行的方式作出数据管理, 由于JDK版本还有其他一些因素, 自己开发了一个运行框架, 并且公布源码 , 希望和大家一个讨论, 改进.
目录
1、概述
1、1、使用的技术
1、1、1:SpringBoot
本人使用的是做为真个的框架的基础
@SpringBootApplication
public class SpringShellJavaApplication {
public static void main(String[] args) {
SpringApplication.run(SpringShellJavaApplication.class, args);
}
}
从这儿就可以看出, 因为使用到了IOC , AOP倒是没有使用到, 如果可以, 你可以自己添加, 所以, 你懂的.
1、1、2: common-cli
不要惊慌, 这个只是是对我们数据的命令还有参数的解析, 所以这儿拿出来只是为了引起注意
1、1、3: maven
这个就节约了, maven 是做到的我们整个项目的构建, 包的管理, 可以很方便的做到这个, 其实, 还有ant, gradle , sbt, 这些都需要自己去折腾的,对maven 比较了解, 深一些
2、使用
源码下载下来之后, 使用的很简单.
2、1:类上面
和 我们的SpringMVC 一样的, 使用注解就可以, 在需要的类上面添加 注解
@ShellComponent(name = "test")
name 是这个类的一个别名, 如果不取, 则会按照当前的类的名字生成默认的命令
2、2:方法上面
在方法上的使用, 和 @PostMapping 这样的注解一样的哦
@ShellMethod(name = "method", detail = "")
public void method(
@ShellOptions(detail = "Ip地址") String address,
@ShellOptions(detail = "端口号") String id,
@ShellOptions(detail = "使用的年限") String year) throws FileNotFoundException {
throw new FileNotFoundException(address + " " + id + " " + year);
}
好啦, 这样就完成了, 是不是很简单
最后的效果是这样的
在Input的后面输入命令就好了, 上面的信息是内置的几个命令, 可以查看全部的命令, 也可以查看某一个命令的详细信息.
如果使用 window 的 cmd 或者是linux的命令行, 这样应该会熟悉.
3、注意事项
3、1: 命令行重复
1、如果在一个@ShellComponent 里面有相同的@ShellMethod,在启动的时候失败.
2、如果在整个项目中, @ShellComponent + @ShellMethod 有两个相同的, 也会启动失败.
3、2: 命令行异常处理
1、输入异常处理:
当前只是添加了基本的管理, 对于参数的验证没有任何的处理, 都当作数据处理, 只是输入简单的字符, 后期后扩展, 输入json, 或者是xml的支持.
输入的处理, 需要调用者自己做处理, 也可以fork 代码, 自己扩展.
2、输出/运行时处理:
对于的异常的处理, 记录在日志中, 提供给后面做排查查阅.还有就是会直接显示在命令上.
第一, 输入异常
命令行输入错误或者是参数输入错误
第二,运行时的异常.
并且查看日志会有如下的记录
3、3 : 日记管理
日志会每天都换一个文件. 并且会对异常分开存储
所有的日志的配置都在这儿, 有详细的说明
3、源码讲解
3、1: 启动
启动很简单, 就是springboot的启动, 可以使用命令启动, 也可以使用 集成工具工具, 还可以使用jar启动, 都可以的
@SpringBootApplication
public class SpringShellJavaApplication {
public static void main(String[] args) {
SpringApplication.run(SpringShellJavaApplication.class, args);
}
}
3、2: 命令行的生成
在启动的时候, spring 运用 ioc, 会给我们初始化需要使用的bean, 单列的哦, 在使用的时候就直接@注解就可以使用了, 但是这儿我门需要去管理一下
在CommandShellConfig 生成了一个@Bean ShellMethodTargetRegistrar 直接使用的new 生成的一个bean哦, 这也是, 一种生成bean的方法, spring 里面也是这样生成的.
然后查看 ShellMethodTargetRegistrar 里面有一个
/**
* 注册方法
*/
@PostConstruct
public void register() {}
看到了吧, 在调用 new ShellMethodTargetRegistrar 的时候, 就会自动执行这个方法, 然后就是扫描整个bean 容器, 找到有 @ShellCompent这个注解, 在找到有 @ShellMethod的方法, 生成命令 还有参数
@PostConstruct
public void register() {
//获取全部的类的信息
Map<String, Object> commandBeans = applicationContext.getBeansWithAnnotation(ShellComponent.class);
//获取方法参数名
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
for (String beanName : commandBeans.keySet()) {
String bName = beanName;
Object bean = commandBeans.get(beanName);
Class<?> clazz = bean.getClass();
ShellComponent shellOptions = clazz.getAnnotation(ShellComponent.class);
if (shellOptions != null && StringUtils.isNoneBlank(shellOptions.name())) {
bName = shellOptions.name();
}
Map<String, Method> methods = Maps.emptys();
List<ShellMethodTarget> shellMethodTargets = Lists.empty();
ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
ShellMethod shellMapping = method.getAnnotation(ShellMethod.class);
String name = shellMapping.name();
if (StringUtils.isEmpty(name)) {
name = method.getName();
}
if (methods.get(name) != null) {
throw new IllegalArgumentException(
String.format("Illegal registration for command '%s': Attempt to register both '%s' and '%s'", name, methods.get(name), method));
}
methods.put(name, method);
String detail = shellMapping.detail();
List<ShellMethodParameter> methodParameterMap = Lists.empty();
String[] param = discoverer.getParameterNames(method);
this.validateShortName(beanName, method, param);
Annotation[][] annotateds = method.getParameterAnnotations();
Annotation[] annotations;
Class[] paramClazzs = method.getParameterTypes();
if (param.length != paramClazzs.length && annotateds.length != paramClazzs.length) {
try {
throw new ShellParameterException(
String.format("Illegal registration for command '%s': All Paramter Must Has Option:@ShellOptions", name)
);
} catch (ShellParameterException e) {
LoggerUtils.error(e.getMessage());
e.printStackTrace();
}
} else {
for (int i = 0; i < param.length; i++) {
annotations = annotateds[i];
methodParameterMap.add(new ShellMethodParameter(param[i], paramClazzs[i], join("", annotations)));
}
ShellMethodTarget target = new ShellMethodTarget(method, bean, name, detail, methodParameterMap);
shellMethodTargets.add(target);
}
}
/**
* 自定义方法
* @param param
*/
private void validateShortName(String name, Method method, String[] param) {
Map<String, String> mSets = Maps.emptys();
String shortName;
if (null != param && param.length > 0) {
for (String s : param) {
shortName = StringUtils.shortName(s);
if (mSets.keySet().contains(shortName)) {
throw new IllegalArgumentException(
String.format("Illegal registration for class '%s' ," +
" method '%s': Attempt to register both parameter option '-%s'(%s) and '-%s'(%s)", name, method, shortName, mSets.get(shortName), shortName, s));
}
mSets.put(shortName, s);
}
}
}
}, new ReflectionUtils.MethodFilter() {
@Override
public boolean matches(Method method) {
return method.getAnnotation(ShellMethod.class) != null;
}
});
commands.put(bName, shellMethodTargets);
}
3、3: 命令的执行
在获取到命令行之后, 解析出参数, 就会去执行 执行在这个类里面:CommandShellRunner.直接就是去调用对应的方法
@Override
public void run(ApplicationArguments applicationArguments) {
PrintWriter pw = new PrintWriter(System.out);
ShellPrint.printFirstMessage();
while (true) {
pw.print("\nInput>:");
pw.flush();
String commandLine = sc.nextLine(); //读取字符串型输入
if (StringUtils.isNotBlank(commandLine)) {
if (ActionEnums.EXIT.getAction().equalsIgnoreCase(commandLine)) {
System.exit(0);
}
try {
if(this.validate(commandLine)){
ShellCommandParse parse = new ShellCommandParse(commands.get(getGroupName(commandLine)));
Object[] args = parse.getParameterValue(commandLine);
if (parse.getCurrentMethod().getBean().getClass().getName().equals("org.poem.core.handler.HelpHandler")) {
args = new Object[1];
String command = getCommand(commandLine);
if (StringUtils.isNotBlank(command)) {
args[0] = command;
}
}
executor(parse.getCurrentMethod(), args);
}
} catch (ParseException e) {
//参数转换异常
LoggerUtils.error(e.getMessage(),e);
ShellPrint.printMsg(e.getMessage());
} catch (ShellCommandException e) {
//输入的命令异常
LoggerUtils.error(e.getMessage(),e);
ShellPrint.printMsg(e.getMessage());
}catch (Exception e){
//调用的方法中出现异常
if(e instanceof UndeclaredThrowableException){
LoggerUtils.error(e);
ShellPrint.printMsg("错误信息:"+ ((UndeclaredThrowableException) e).getUndeclaredThrowable().getLocalizedMessage());
}
else{
LoggerUtils.error(e);
ShellPrint.printMsg(e.getMessage());
}
}
} else {
System.err.println("\n");
}
}
}
这儿有一个while(true) 一直等着命令的输入, 然后去解析, 在执行方法, 这儿使用了反射去获取调用的类, 调用的方法
/**
* 执行器
*
* @param shellMethodTarget
* @param parameters
*/
private void executor(ShellMethodTarget shellMethodTarget, Object[] parameters) {
try {
Object clsObj = shellMethodTarget.getBean();
Method method = shellMethodTarget.getMethod();
String name = method.getReturnType().getSimpleName();
if (!"void".equals(name)) {
Object result = ReflectionUtils.invokeMethod(method, clsObj, parameters);
ShellPrint.printResult(result);
} else {
ReflectionUtils.invokeMethod(method, clsObj, parameters);
}
} catch (IllegalArgumentException e) {
LoggerUtils.error(e);
ShellPrint.printMsg(e.getMessage());
} catch (Exception e) {
LoggerUtils.error(e);
if (e instanceof UndeclaredThrowableException) {
ShellPrint.printMsg("错误信息:" + ((UndeclaredThrowableException) e).getUndeclaredThrowable().getLocalizedMessage());
} else {
ShellPrint.printMsg(e.getMessage());
}
} finally {
//打印数据
ShellPrint.printMsg("\n");
}
}
3、4: 输出
1、正常的输出:
正常输出, 是用户自己的输出, 或者是集成SObject, 直接输出, 可以覆盖掉SObject, 自定义输出
/**
* super class
*/
public class SObject implements Serializable {
/**
* default toString
* @return
*/
public String sToString(){
return super.toString();
}
}
2、异常输出:
异常输出, 是代码运行错误的输出, 会打印出对于的错误, 并且, 会记录在日志里.
catch (Exception e) {
LoggerUtils.error(e);
if (e instanceof UndeclaredThrowableException) {
ShellPrint.printMsg("错误信息:" + ((UndeclaredThrowableException) e).getUndeclaredThrowable().getLocalizedMessage());
} else {
ShellPrint.printMsg(e.getMessage());
}
完成.
后续会增加json, 生成文档的功能, 请期待, 如果有问题, 可以在github上或者在文章下写.
多交流