ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

springshell无参异常中文显示

2022-05-25 01:33:51  阅读:163  来源: 互联网

标签:无参 中文 return springframework methodParameter private springshell org parameter


在搭建springshell工程时,在需要参数的方法操作时,如误忘填写参数,显示出全英文提示,在只提供国内服务时对部分阅读者不友好,故重写实现;

原操作时提示(输入stacktrace查看详细信息):

SpringShellDemo@LAPTOP-JSP5MRO1> sum
Parameter '--a int' should be specified
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
SpringShellDemo@LAPTOP-JSP5MRO1> stacktrace
org.springframework.shell.ParameterMissingResolutionException: Parameter '--a int' should be specified
    at org.springframework.shell.standard.StandardParameterResolver.resolve(StandardParameterResolver.java:227)
    at org.springframework.shell.Shell.resolveArgs(Shell.java:296)
    at org.springframework.shell.Shell.evaluate(Shell.java:167)
    at org.springframework.shell.Shell.run(Shell.java:134)
    at org.springframework.shell.jline.InteractiveShellApplicationRunner.run(InteractiveShellApplicationRunner.java:84)
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:768)
    at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:758)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:310)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1312)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1301)
    at com.example.springshelldemo.SpringShellDemoApplication.main(SpringShellDemoApplication.java:13)

现重写接口ParameterResolver实现类SpringShellStandardParameterResolver以覆盖:

package com.example.springshelldemo.config;

import com.example.springshelldemo.common.ShellParameterMissingResolutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.shell.*;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
import org.springframework.shell.standard.ValueProvider;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ObjectUtils;

import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.metadata.MethodDescriptor;
import javax.validation.metadata.ParameterDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.springframework.shell.Utils.unCamelify;

@Component
public class SpringShellStandardParameterResolver implements ParameterResolver {

    private final ConversionService conversionService;

    private Collection<ValueProvider> valueProviders = new HashSet<>();

    /**
     * A cache from method+input to String representation of actual parameter values. Note
     * that the converted result is not cached, to allow dynamic computation to happen at
     * every invocation if needed (e.g. if a remote service is involved).
     */
    private final Map<SpringShellStandardParameterResolver.CacheKey, Map<Parameter, SpringShellStandardParameterResolver.ParameterRawValue>> parameterCache = new ConcurrentReferenceHashMap<>();

    @Autowired
    public SpringShellStandardParameterResolver(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    @Autowired(required = false)
    public void setValueProviders(Collection<ValueProvider> valueProviders) {
        this.valueProviders = valueProviders;
    }

    private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    @Autowired(required = false)
    public void setValidatorFactory(ValidatorFactory validatorFactory) {
        this.validator = validatorFactory.getValidator();
    }


    @Override
    public boolean supports(MethodParameter parameter) {
        boolean optOut = parameter.hasParameterAnnotation(ShellOption.class)
                && parameter.getParameterAnnotation(ShellOption.class).optOut();
        return !optOut && parameter.getMethodAnnotation(ShellMethod.class) != null;
    }

    @Override
    public ValueResult resolve(MethodParameter methodParameter, List<String> wordsBuffer) {
        String prefix = prefixForMethod(methodParameter.getMethod());

        List<String> words = wordsBuffer.stream().filter(w -> !w.isEmpty()).collect(Collectors.toList());

        SpringShellStandardParameterResolver.CacheKey cacheKey = new SpringShellStandardParameterResolver.CacheKey(methodParameter.getMethod(), wordsBuffer);
        parameterCache.clear();
        Map<Parameter, SpringShellStandardParameterResolver.ParameterRawValue> resolved = parameterCache.computeIfAbsent(cacheKey, (k) -> {

            Map<Parameter, SpringShellStandardParameterResolver.ParameterRawValue> result = new HashMap<>();
            Map<String, String> namedParameters = new HashMap<>();

            // index of words that haven't yet been used to resolve parameter values
            List<Integer> unusedWords = new ArrayList<>();

            Set<String> possibleKeys = gatherAllPossibleKeys(methodParameter.getMethod());

            // First, resolve all parameters passed by-name
            for (int i = 0; i < words.size(); i++) {
                int from = i;
                String word = words.get(i);
                if (possibleKeys.contains(word)) {
                    String key = word;
                    Parameter parameter = lookupParameterForKey(methodParameter.getMethod(), key);
                    int arity = getArity(parameter);

                    if (i + 1 + arity > words.size()) {
                        String input = words.subList(i, words.size()).stream().collect(Collectors.joining(" "));
                        throw new UnfinishedParameterResolutionException(
                                describe(Utils.createMethodParameter(parameter)).findFirst().get(), input);
                    }
                    Assert.isTrue(i + 1 + arity <= words.size(),
                            String.format("Not enough input for parameter '%s'", word));
                    String raw = words.subList(i + 1, i + 1 + arity).stream().collect(Collectors.joining(","));
                    Assert.isTrue(!namedParameters.containsKey(key),
                            String.format("Parameter for '%s' has already been specified", word));
                    namedParameters.put(key, raw);
                    if (arity == 0) {
                        boolean defaultValue = booleanDefaultValue(parameter);
                        // Boolean parameter has been specified. Use the opposite of the default value
                        result.put(parameter,
                                SpringShellStandardParameterResolver.ParameterRawValue.explicit(String.valueOf(!defaultValue), key, from, from));
                    } else {
                        i += arity;
                        result.put(parameter, SpringShellStandardParameterResolver.ParameterRawValue.explicit(raw, key, from, i));
                    }
                } // store for later processing of positional params
                else {
                    unusedWords.add(i);
                }
            }

            // Now have a second pass over params and treat them as positional
            int offset = 0;
            Parameter[] parameters = methodParameter.getMethod().getParameters();
            for (int i = 0, parametersLength = parameters.length; i < parametersLength; i++) {
                Parameter parameter = parameters[i];
                // Compute the intersection between possible keys for the param and what we've already
                // seen for named params
                Collection<String> keys = getKeysForParameter(methodParameter.getMethod(), i)
                        .collect(Collectors.toSet());
                Collection<String> copy = new HashSet<>(keys);
                copy.retainAll(namedParameters.keySet());
                if (copy.isEmpty()) { // Was not set via a key (including aliases), must be positional
                    int arity = getArity(parameter);
                    if (arity > 0 && (offset + arity) <= unusedWords.size()) {
                        String raw = unusedWords.subList(offset, offset + arity).stream()
                                .map(index -> words.get(index))
                                .collect(Collectors.joining(","));
                        int from = unusedWords.get(offset);
                        int to = from + arity - 1;
                        result.put(parameter, SpringShellStandardParameterResolver.ParameterRawValue.explicit(raw, null, from, to));
                        offset += arity;
                    } // No more input. Try defaultValues
                    else {
                        Optional<String> defaultValue = defaultValueFor(parameter);
                        defaultValue.ifPresent(
                                value -> result.put(parameter, SpringShellStandardParameterResolver.ParameterRawValue.implicit(value, null, null, null)));
                    }
                } else if (copy.size() > 1) {
                    throw new IllegalArgumentException(
                            "Named parameter has been specified multiple times via " + quote(copy));
                }
            }

            Assert.isTrue(offset == unusedWords.size(),
                    "Too many arguments: the following could not be mapped to parameters: "
                            + unusedWords.subList(offset, unusedWords.size()).stream()
                            .map(index -> words.get(index)).collect(Collectors.joining(" ", "'", "'")));
            return result;
        });

        Parameter param = methodParameter.getMethod().getParameters()[methodParameter.getParameterIndex()];
        if (!resolved.containsKey(param)) {
            throw new ShellParameterMissingResolutionException(describe(methodParameter).findFirst().get());
        }
        SpringShellStandardParameterResolver.ParameterRawValue parameterRawValue = resolved.get(param);
        Object value = convertRawValue(parameterRawValue, methodParameter);
        BitSet wordsUsed = getWordsUsed(parameterRawValue);
        BitSet wordsUsedForValue = getWordsUsedForValue(parameterRawValue);
        return new ValueResult(methodParameter, value, wordsUsed, wordsUsedForValue);
    }

    private BitSet getWordsUsed(SpringShellStandardParameterResolver.ParameterRawValue parameterRawValue) {
        if (parameterRawValue.from != null) {
            BitSet wordsUsed = new BitSet();
            wordsUsed.set(parameterRawValue.from, parameterRawValue.to + 1);
            return wordsUsed;
        }
        return null;
    }

    private BitSet getWordsUsedForValue(SpringShellStandardParameterResolver.ParameterRawValue parameterRawValue) {
        if (parameterRawValue.from != null) {
            BitSet wordsUsedForValue = new BitSet();
            wordsUsedForValue.set(parameterRawValue.from, parameterRawValue.to + 1);
            if (parameterRawValue.key != null) {
                wordsUsedForValue.clear(parameterRawValue.from);
            }
            return wordsUsedForValue;
        }
        return null;
    }

    private Object convertRawValue(SpringShellStandardParameterResolver.ParameterRawValue parameterRawValue, MethodParameter methodParameter) {
        String s = parameterRawValue.value;
        if (ShellOption.NULL.equals(s)) {
            return null;
        } else {
            return conversionService.convert(s, TypeDescriptor.valueOf(String.class),
                    new TypeDescriptor(methodParameter));
        }
    }

    private Set<String> gatherAllPossibleKeys(Method method) {
        return Arrays.stream(method.getParameters())
                .flatMap(this::getKeysForParameter)
                .collect(Collectors.toSet());
    }

    private String prefixForMethod(Executable method) {
        return method.getAnnotation(ShellMethod.class).prefix();
    }

    private Optional<String> defaultValueFor(Parameter parameter) {
        ShellOption option = parameter.getAnnotation(ShellOption.class);
        if (option != null && !ShellOption.NONE.equals(option.defaultValue())) {
            return Optional.of(option.defaultValue());
        } else if (getArity(parameter) == 0) {
            return Optional.of("false");
        }
        return Optional.empty();
    }

    private boolean booleanDefaultValue(Parameter parameter) {
        ShellOption option = parameter.getAnnotation(ShellOption.class);
        if (option != null && !ShellOption.NONE.equals(option.defaultValue())) {
            return Boolean.parseBoolean(option.defaultValue());
        }
        return false;
    }

    @Override
    public Stream<ParameterDescription> describe(MethodParameter parameter) {
        Parameter jlrParameter = parameter.getMethod().getParameters()[parameter.getParameterIndex()];
        int arity = getArity(jlrParameter);
        Class<?> type = parameter.getParameterType();
        ShellOption option = jlrParameter.getAnnotation(ShellOption.class);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < arity; i++) {
            if (i > 0) {
                sb.append(" ");
            }
            sb.append(arity > 1 ? unCamelify(removeMultiplicityFromType(parameter).getSimpleName())
                    : unCamelify(type.getSimpleName()));
        }
        ParameterDescription result = ParameterDescription.outOf(parameter);
        result.formal(sb.toString());
        if (option != null) {
            result.help(option.help());
            Optional<String> defaultValue = defaultValueFor(jlrParameter);
            if (defaultValue.isPresent()) {
                result.defaultValue(defaultValue.map(dv -> dv.equals(ShellOption.NULL) ? "<none>" : dv).get());
            }
        }
        result
                .keys(getKeysForParameter(parameter.getMethod(), parameter.getParameterIndex())
                        .collect(Collectors.toList()))
                .mandatoryKey(false);

        MethodDescriptor constraintsForMethod = validator.getConstraintsForClass(parameter.getDeclaringClass())
                .getConstraintsForMethod(parameter.getMethod().getName(), parameter.getMethod().getParameterTypes());
        if (constraintsForMethod != null) {
            ParameterDescriptor constraintsDescriptor = constraintsForMethod
                    .getParameterDescriptors().get(parameter.getParameterIndex());
            result.elementDescriptor(constraintsDescriptor);
        }

        return Stream.of(result);
    }

    @Override
    public List<CompletionProposal> complete(MethodParameter methodParameter, CompletionContext context) {
        boolean set;
        Exception unfinished = null;
        // First try to see if this parameter has been set, even to some unfinished value
        SpringShellStandardParameterResolver.ParameterRawValue parameterRawValue = null;
        int arity = 1;
        try {
            resolve(methodParameter, context.getWords());
            SpringShellStandardParameterResolver.CacheKey cacheKey = new SpringShellStandardParameterResolver.CacheKey(methodParameter.getMethod(), context.getWords());
            Parameter parameter = methodParameter.getMethod().getParameters()[methodParameter.getParameterIndex()];
            arity = getArity(parameter);
            parameterRawValue = parameterCache.get(cacheKey).get(parameter);
            set = parameterRawValue.explicit;
        } catch (ShellParameterMissingResolutionException e) {
            set = false;
        } catch (UnfinishedParameterResolutionException e) {
            if (e.getParameterDescription().parameter().equals(methodParameter)) {
                unfinished = e;
                set = false;
            } else {
                return Collections.emptyList();
            }
        } catch (Exception e) {
            // Most likely what is already typed would fail resolution (eg type conversion failure)
            return argumentKeysThatStartWithContextPrefix(methodParameter, context);
        }

        // There are 4 possible cases:
        // 1) parameter not set at all
        // 2) parameter set via its key, not enough input to consume a value
        // 3) parameter set with multiple values, enough to cover arity. We're done
        // 4) parameter set, and some value bound. But maybe that value is just a prefix to what
        // the user actually wants
        // 4.1) or maybe that value was resolved by position, but is a prefix of an actual valid
        // key

        if (!set) {
            if (unfinished == null) {
                // case 1 above
                return argumentKeysThatStartWithContextPrefix(methodParameter, context);
            } // case 2
            else {
                return valueCompletions(methodParameter, context);
            }
        } else {
            List<CompletionProposal> result = new ArrayList<>();

            Object value = convertRawValue(parameterRawValue, methodParameter);
            if (value instanceof Collection && ((Collection) value).size() == arity
                    || (ObjectUtils.isArray(value) && Array.getLength(value) == arity)) {
                // We're done already
                return result;
            }
            if (!context.currentWord().equals("")) {
                // Case 4
                result.addAll(valueCompletions(methodParameter, context));
            }

            if (parameterRawValue.positional()) {
                // Case 4.1: There exists "--command foo" and user has typed "--comm" which (wrongly) got
                // resolved as a positional param
                result.addAll(argumentKeysThatStartWithContextPrefix(methodParameter, context));
            }
            return result;
        }
    }

    private List<CompletionProposal> valueCompletions(MethodParameter methodParameter,
                                                      CompletionContext completionContext) {
        return valueProviders.stream()
                .filter(vp -> vp.supports(methodParameter, completionContext))
                .map(vp -> vp.complete(methodParameter, completionContext, null))
                .findFirst().orElseGet(() -> Collections.emptyList());
    }

    private List<CompletionProposal> argumentKeysThatStartWithContextPrefix(MethodParameter methodParameter,
                                                                            CompletionContext context) {
        String prefix = context.currentWordUpToCursor() != null ? context.currentWordUpToCursor() : "";
        return describe(methodParameter).flatMap(pd -> pd.keys().stream())
                .filter(k -> k.startsWith(prefix))
                .map(CompletionProposal::new)
                .collect(Collectors.toList());
    }

    /**
     * In case of {@code foo[] or Collection<Foo>} and arity > 1, return the element type.
     */
    private Class<?> removeMultiplicityFromType(MethodParameter parameter) {
        Class<?> parameterType = parameter.getParameterType();
        if (parameterType.isArray()) {
            return parameterType.getComponentType();
        } else if (Collection.class.isAssignableFrom(parameterType)) {
            return parameter.getNestedParameterType();
        } else {
            throw new RuntimeException("For " + parameter + " (with arity > 1) expected an array/collection type");
        }
    }

    /**
     * Surrounds the parameter keys with quotes.
     */
    private String quote(Collection<String> keys) {
        return keys.stream().collect(Collectors.joining(", ", "'", "'"));
    }

    /**
     * Return the arity of a given parameter. The default arity is 1, except for booleans
     * where arity is 0 (can be overridden back to 1 via an annotation)
     */
    private int getArity(Parameter parameter) {
        ShellOption option = parameter.getAnnotation(ShellOption.class);
        int inferred = (parameter.getType() == boolean.class || parameter.getType() == Boolean.class) ? 0 : 1;
        return option != null && option.arity() != ShellOption.ARITY_USE_HEURISTICS ? option.arity() : inferred;
    }

    /**
     * Return the key(s) for the i-th parameter of the command method, resolved either from
     * the {@link ShellOption} annotation, or from the actual parameter name.
     */
    private Stream<String> getKeysForParameter(Method method, int index) {
        Parameter p = method.getParameters()[index];
        return getKeysForParameter(p);
    }

    private Stream<String> getKeysForParameter(Parameter p) {
        Executable method = p.getDeclaringExecutable();
        String prefix = prefixForMethod(method);
        ShellOption option = p.getAnnotation(ShellOption.class);
        if (option != null && option.value().length > 0) {
            return Arrays.stream(option.value());
        } else {
            return Stream.of(prefix + Utils.unCamelify(Utils.createMethodParameter(p).getParameterName()));
        }
    }

    /**
     * Return the method parameter that should be bound to the given key.
     */
    private Parameter lookupParameterForKey(Method method, String key) {
        Parameter[] parameters = method.getParameters();
        for (int i = 0, parametersLength = parameters.length; i < parametersLength; i++) {
            Parameter p = parameters[i];
            if (getKeysForParameter(method, i).anyMatch(k -> k.equals(key))) {
                return p;
            }
        }
        throw new IllegalArgumentException(String.format("Could not look up parameter for '%s' in %s", key, method));
    }

    private static class CacheKey {

        private final Method method;

        private final List<String> words;

        private CacheKey(Method method, List<String> words) {
            this.method = method;
            this.words = words;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;
            SpringShellStandardParameterResolver.CacheKey cacheKey = (SpringShellStandardParameterResolver.CacheKey) o;
            return Objects.equals(method, cacheKey.method) &&
                    Objects.equals(words, cacheKey.words);
        }

        @Override
        public int hashCode() {
            return Objects.hash(method, words);
        }

        @Override
        public String toString() {
            return method.getName() + " " + words;
        }
    }

    private static class ParameterRawValue {

        private CompletionContext context;

        private Integer from;

        private Integer to;

        private Integer keyIndex;

        /**
         * The raw String value that got bound to a parameter.
         */
        private final String value;

        /**
         * If false, the value resolved is the result of applying defaults.
         */
        private final boolean explicit;

        /**
         * The key that was used to set the parameter, or null if resolution happened by position.
         */
        private final String key;

        private ParameterRawValue(String value, boolean explicit, String key, Integer from, Integer to) {
            this.value = value;
            this.explicit = explicit;
            this.key = key;
            this.from = from;
            this.to = to;
        }

        public static SpringShellStandardParameterResolver.ParameterRawValue explicit(String value, String key, Integer from, Integer to) {
            return new SpringShellStandardParameterResolver.ParameterRawValue(value, true, key, from, to);
        }

        public static SpringShellStandardParameterResolver.ParameterRawValue implicit(String value, String key, Integer from, Integer to) {
            return new SpringShellStandardParameterResolver.ParameterRawValue(value, false, key, from, to);
        }

        public boolean positional() {
            return key == null;
        }

        @Override
        public String toString() {
            return "ParameterRawValue{" +
                    "value='" + value + '\'' +
                    ", explicit=" + explicit +
                    ", key='" + key + '\'' +
                    ", from=" + from +
                    ", to=" + to +
                    '}';
        }
    }
}

编写异常ShellParameterMissingResolutionException已达到替换ParameterMissingResolutionException目的:

package com.example.springshelldemo.common;

import org.springframework.shell.ParameterDescription;

public class ShellParameterMissingResolutionException extends RuntimeException{
    private final ParameterDescription parameterDescription;

    public ShellParameterMissingResolutionException(ParameterDescription parameterDescription) {
        this.parameterDescription = parameterDescription;
    }

    public ParameterDescription getParameterDescription() {
        return parameterDescription;
    }

    // "Parameter '%s' should be specified"
    @Override
    public String getMessage() {
        return String.format("参数 '%s' 需指定", parameterDescription);
    }
}

实现后操作提示:

SpringShellDemo@LAPTOP-JSP5MRO1> sum
参数 '--a int' 需指定
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
SpringShellDemo@LAPTOP-JSP5MRO1> stacktrace
com.example.springshelldemo.common.ShellParameterMissingResolutionException: 参数 '--a int' 需指定
    at com.example.springshelldemo.config.SpringShellStandardParameterResolver.resolve(SpringShellStandardParameterResolver.java:168)
    at org.springframework.shell.Shell.resolveArgs(Shell.java:296)
    at org.springframework.shell.Shell.evaluate(Shell.java:167)
    at org.springframework.shell.Shell.run(Shell.java:134)
    at org.springframework.shell.jline.InteractiveShellApplicationRunner.run(InteractiveShellApplicationRunner.java:84)
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:768)
    at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:758)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:310)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1312)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1301)
    at com.example.springshelldemo.SpringShellDemoApplication.main(SpringShellDemoApplication.java:13)qita 

其他异常亦可使用相同方法重写替换;

标签:无参,中文,return,springframework,methodParameter,private,springshell,org,parameter
来源: https://www.cnblogs.com/chencoolandclear/p/16307845.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有