系统相关
首页 > 系统相关> > springshell无参异常中文显示

springshell无参异常中文显示

作者:互联网

在搭建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