背景知识
几个月前,任务是编写一个与公司产品一起使用的HTTP API,即使用RestTemplate包装所有请求,然后从应用程序中拦截信息并修改响应。 与该应用程序一起使用的服务的大致实现如下:
if (headers == null) { headers = new HttpHeaders(); } if (headers.getFirst("Content-Type") == null) { headers.add("Content-Type", MediaType.APPLICATION_JSON_VALUE); } HttpEntity<Object> entity; if (body == null) { entity = new HttpEntity<>(headers); } else { entity = new HttpEntity<>(body, headers); } final String uri = String.format("%s%s/%s", workingUrl, apiPath, request.info()); final Class<O> type = (Class<O>) request.type(); final O response = (O)restTemplate.exchange(uri, request.method(), entity, type);
...接受类型,正文和请求标头的简单方法。 一切都会好起来的,但是看起来像拐杖,在Spring的上下文中不是很有用。
当同事们在旧机制的分支机构中编写函数时,我有一个最巧妙的想法-为什么不“单行”编写这些请求(如Feign)。
主意
我们拥有一个功能强大的Spring DI容器,那么为什么不充分利用其功能呢? 特别是,使用Jpa示例初始化Data存储库。 我面临的任务是在Spring的上下文中初始化接口类型的类,以及用于拦截作为典型实现的方法调用的三种解决方案-Aspect,PostProcess和BeanDefinitionRegistrar。
代码库
首先是注释,没有注释的地方,否则如何配置查询。
1)映射-将接口标识为HTTP调用组成部分的注释。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Mapping { String alias(); }
alias参数负责分配服务的根路由,例如https : //habr.com,https://github.com等。
2)ServiceMapping-一种注解,用于标识应作为对应用程序的标准HTTP请求调用的接口方法,我们希望从该接口处获得响应或执行某些操作。
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface ServiceMapping { String path(); HttpMethod method(); Header[] defaultHeaders() default {}; Class<?> fallbackClass() default Object.class; String fallbackMethod() default ""; }
参数:
- path-请求路径,例如别名+ / ru / hub / $ {hub_name};
- 方法 -HTTP请求方法(GET,POST,PUT等);
- defaultHeaders-对于远程资源(Content-Type,Accept等)不可变的静态请求标头;
- fallbackClass-拒绝处理的带有错误(异常)的请求的类;
- fallbackMethod-发生错误时应返回正确结果的类方法的名称(异常)。
3)标头-注释,用于标识请求设置中的静态标头
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.ANNOTATION_TYPE}) @Documented public @interface Header { String name(); String value(); }
参数:
下一步是实现FactoryBean,以拦截接口方法的调用。
MappingFactoryBean.java package org.restclient.factory; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import org.restclient.annotations.RestInterceptor; import org.restclient.annotations.ServiceMapping; import org.restclient.annotations.Type; import org.restclient.config.ServicesConfiguration; import org.restclient.config.ServicesConfiguration.RouteSettings; import org.restclient.interceptor.Interceptor; import org.restclient.model.MappingMetadata; import org.restclient.model.Pair; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.jmx.access.InvocationFailureException; import org.springframework.lang.NonNull; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.client.RestClientResponseException; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient.ResponseSpec; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import javax.naming.ConfigurationException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.*; @Slf4j @ToString public class MappingFactoryBean implements BeanFactoryAware, FactoryBean<Object>, ApplicationContextAware { private static final Collection<String> ignoredMethods = Arrays.asList("equals", "hashCode", "toString"); private Class<?> type; private List<Object> fallbackInstances; private List<MappingMetadata> metadatas; private String alias; private ApplicationContext applicationContext; private BeanFactory beanFactory; @Override public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } @Override public Class<?> getObjectType() { return type; } @Override public boolean isSingleton() { return true; } @Override public Object getObject() { return Enhancer.create(type, (MethodInterceptor) (instance, method, args, methodProxy) -> { final boolean skip = ignoredMethods.stream().anyMatch(ignore -> method.getName().equals(ignore)); final ServiceMapping annotation = method.getAnnotation(ServiceMapping.class); if (!skip && annotation != null) { return invokeMethod(annotation, method, args); } return null; }); } private Object invokeMethod(ServiceMapping annotation, Method method, Object[] args) throws Throwable { final MappingMetadata metadata = findMetadataByMethodName(method.getName()); if (metadata == null) { throw new NoSuchMethodException(String.format("Cant find metadata for method %s. Check your mapping configuration!", method.getName())); } final RouteSettings routeSettings = findSettingsByAlias(alias); final String host = routeSettings.getHost(); String url = metadata.getUrl().replace(String.format("${%s}", alias), host); final HttpMethod httpMethod = metadata.getHttpMethod(); final HttpHeaders httpHeaders = metadata.getHttpHeaders(); final List<Pair<String, Object>> foundVars = new ArrayList<>(); final List<Pair<String, Object>> foundParams = new ArrayList<>(); final List<Pair<String, Object>> foundHeaders = new ArrayList<>(); final Parameter[] parameters = method.getParameters(); final Object body = initHttpVariables(args, parameters, foundVars, foundParams, foundHeaders); url = replaceHttpVariables(url, foundVars, foundParams, foundHeaders, httpHeaders); preHandle(args, body, httpHeaders); if (log.isDebugEnabled()) { log.debug("Execute Service Mapping request"); log.debug("Url: {}", url); log.debug("Headers: {}", httpHeaders); if (body != null) { log.debug("Body: {}", body); } } final Object call = handleHttpCall(annotation, args, url, httpMethod, body, httpHeaders, metadata); postHandle(ResponseEntity.ok(call)); return call; } private Object handleHttpCall(ServiceMapping annotation, Object[] args, String url, HttpMethod httpMethod, Object body, HttpHeaders httpHeaders, MappingMetadata metadata) throws Throwable { final WebClient webClient = WebClient.create(url); ResponseSpec responseSpec; final Class<?> returnType = metadata.getReturnType(); try { if (body != null) { responseSpec = webClient .method(httpMethod) .headers(c -> c.addAll(httpHeaders)) .body(BodyInserters.fromPublisher(Mono.just(body), Object.class)) .retrieve(); } else { responseSpec = webClient .method(httpMethod) .headers(c -> c.addAll(httpHeaders)) .retrieve(); } } catch (RestClientResponseException ex) { if (log.isDebugEnabled()) { log.debug("Error on execute route request - Code: {}, Error: {}, Route: {}", ex.getRawStatusCode(), ex.getResponseBodyAsString(), url); } final String fallbackMethod = metadata.getFallbackMethod(); final Object target = fallbackInstances.stream() .filter(o -> o.getClass().getSimpleName().equals(annotation.fallbackClass().getSimpleName())) .findFirst().orElse(null); Method fallback = null; if (target != null) { fallback = Arrays.stream(target.getClass().getMethods()) .filter(m -> m.getName().equals(fallbackMethod)) .findFirst() .orElse(null); } if (fallback != null) { args = Arrays.copyOf(args, args.length + 1); args[args.length - 1] = ex; final Object result = fallback.invoke(target, args); return Mono.just(result); } else if (returnType == Mono.class) { return Mono.just(ResponseEntity.status(ex.getRawStatusCode()).body(ex.getResponseBodyAsString())); } else if (returnType == Flux.class) { return Flux.just(ResponseEntity.status(ex.getRawStatusCode()).body(ex.getResponseBodyAsString())); } else { return Mono.empty(); } } final Method method = metadata.getMethod(); final Type classType = method.getDeclaredAnnotation(Type.class); final Class<?> type = classType == null ? Object.class : classType.type(); if (returnType == Mono.class) { return responseSpec.bodyToMono(type); } else if (returnType == Flux.class) { return responseSpec.bodyToFlux(type); } return null; } private String replaceHttpVariables(String url, final List<Pair<String, Object>> foundVars, final List<Pair<String, Object>> foundParams, final List<Pair<String, Object>> foundHeaders, final HttpHeaders httpHeaders) { for (Pair<String, Object> pair : foundVars) { url = url.replace(String.format("${%s}", pair.getKey()), String.valueOf(pair.getValue())); } for (Pair<String, Object> pair : foundParams) { url = url.replace(String.format("${%s}", pair.getKey()), String.valueOf(pair.getValue())); } foundHeaders.forEach(pair -> { final String headerName = pair.getKey(); if (httpHeaders.getFirst(headerName) != null) { httpHeaders.set(headerName, String.valueOf(pair.getValue())); } else { log.warn("Undefined request header name '{}'! Check mapping configuration!", headerName); } }); return url; } private Object initHttpVariables(final Object[] args, final Parameter[] parameters, final List<Pair<String, Object>> foundVars, final List<Pair<String, Object>> foundParams, final List<Pair<String, Object>> foundHeaders) { Object body = null; for (int i = 0; i < parameters.length; i++) { final Object value = args[i]; final Parameter parameter = parameters[i]; final PathVariable pv = parameter.getDeclaredAnnotation(PathVariable.class); final RequestParam rp = parameter.getDeclaredAnnotation(RequestParam.class); final RequestHeader rh = parameter.getDeclaredAnnotation(RequestHeader.class); final RequestBody rb = parameter.getDeclaredAnnotation(RequestBody.class); if (rb != null) { body = value; } if (rh != null) { foundHeaders.add(new Pair<>(rh.value(), value)); } if (pv != null) { final String name = pv.value(); foundVars.add(new Pair<>(name, value)); } if (rp != null) { final String name = rp.value(); foundParams.add(new Pair<>(name, value)); } } return body; } private void preHandle(Object[] args, Object body, HttpHeaders httpHeaders) { final Map<String, Interceptor> beansOfType = applicationContext.getBeansOfType(Interceptor.class); beansOfType.values() .stream() .filter(i -> i.getClass().isAnnotationPresent(RestInterceptor.class) && ArrayUtils.contains(i.getClass().getDeclaredAnnotation(RestInterceptor.class).aliases(), alias)) .forEach(i -> i.preHandle(args, body, httpHeaders)); } private void postHandle(ResponseEntity<?> responseEntity) { final Map<String, Interceptor> beansOfType = applicationContext.getBeansOfType(Interceptor.class); beansOfType.values() .stream() .filter(i -> i.getClass().isAnnotationPresent(RestInterceptor.class) && ArrayUtils.contains(i.getClass().getDeclaredAnnotation(RestInterceptor.class).aliases(), alias)) .forEach(i -> i.postHandle(responseEntity)); } private MappingMetadata findMetadataByMethodName(String methodName) { return metadatas .stream() .filter(m -> m.getMethodName().equals(methodName)).findFirst() .orElseThrow(() -> new InvocationFailureException("")); } private RouteSettings findSettingsByAlias(String alias) throws ConfigurationException { final ServicesConfiguration servicesConfiguration = applicationContext.getAutowireCapableBeanFactory().getBean(ServicesConfiguration.class); return servicesConfiguration.getRoutes() .stream() .filter(r -> r.getAlias().equals(alias)) .findFirst() .orElseThrow(() -> new ConfigurationException(String.format("Cant find service host! Check configuration. Alias: %s", alias))); } @SuppressWarnings("unused") public Class<?> getType() { return type; } @SuppressWarnings("unused") public void setType(Class<?> type) { this.type = type; } @SuppressWarnings("unused") public List<MappingMetadata> getMetadatas() { return metadatas; } @SuppressWarnings("unused") public void setMetadatas(List<MappingMetadata> metadatas) { this.metadatas = metadatas; } @SuppressWarnings("unused") public String getAlias() { return alias; } @SuppressWarnings("unused") public void setAlias(String alias) { this.alias = alias; } @SuppressWarnings("unused") public List<Object> getFallbackInstances() { return fallbackInstances; } @SuppressWarnings("unused") public void setFallbackInstances(List<Object> fallbackInstances) { this.fallbackInstances = fallbackInstances; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MappingFactoryBean that = (MappingFactoryBean) o; return Objects.equals(type, that.type); } @Override public int hashCode() { return Objects.hash(type); } }
我将简要解释bin对象的此实现是什么:
- 提供具有资源请求设置的接口方法的元信息存储,例如通过注释,拒绝类,路由设置模型的集合标识的方法本身;
- 提供使用CGlib(MappingFactoryBean#getObject())在应用程序上下文中拦截方法调用的功能,即 正式地,没有实现被调用方法,但是该方法在物理上被拦截,并且根据参数,注释和方法参数,将处理HTTP请求。
第三步是实现DI容器Spring的低级组件,特别是ImportBeanDefinitionRegistrar接口。
ServiceMappingRegistrator.java package org.restclient.factory; import lombok.extern.slf4j.Slf4j; import org.restclient.annotations.Header; import org.restclient.annotations.Mapping; import org.restclient.annotations.ServiceMapping; import org.restclient.model.MappingMetadata; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.EnvironmentAware; import org.springframework.context.ResourceLoaderAware; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.ClassMetadata; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.lang.NonNull; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import javax.naming.ConfigurationException; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.*; import java.util.stream.Collectors; @Slf4j public class ServiceMappingRegistrator implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { private ResourceLoader resourceLoader; private Environment environment; @Override public void setEnvironment(@NonNull Environment environment) { this.environment = environment; } @Override public void setResourceLoader(@NonNull ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } @Override public void registerBeanDefinitions(@NonNull AnnotationMetadata metadata, @NonNull BeanDefinitionRegistry registry) { registerMappings(metadata, registry); } private void registerMappings(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { final ClassPathScanningCandidateComponentProvider scanner = getScanner(); scanner.setResourceLoader(resourceLoader); final Set<String> basePackages = getBasePackages(metadata); final AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(Mapping.class); scanner.addIncludeFilter(annotationTypeFilter); basePackages .stream() .map(scanner::findCandidateComponents) .flatMap(Collection::stream) .filter(candidateComponent -> candidateComponent instanceof AnnotatedBeanDefinition) .map(candidateComponent -> (AnnotatedBeanDefinition) candidateComponent) .map(AnnotatedBeanDefinition::getMetadata) .map(ClassMetadata::getClassName) .forEach(className -> buildGateway(className, registry)); } private void buildGateway(String className, BeanDefinitionRegistry registry) { try { final Class<?> type = Class.forName(className); final List<Method> methods = Arrays .stream(type.getMethods()) .filter(method -> method.isAnnotationPresent(ServiceMapping.class)) .collect(Collectors.toList()); final String alias = type.getDeclaredAnnotation(Mapping.class).alias(); final List<MappingMetadata> metadatas = new ArrayList<>(); final List<Object> fallbackInstances = new ArrayList<>(); for (Method method : methods) { final ServiceMapping serviceMapping = method.getDeclaredAnnotation(ServiceMapping.class); final Class<?>[] args = method.getParameterTypes(); final Header[] defaultHeaders = serviceMapping.defaultHeaders(); final String path = serviceMapping.path(); final HttpMethod httpMethod = serviceMapping.method(); final HttpHeaders httpHeaders = new HttpHeaders(); final StringBuilder url = new StringBuilder(); url.append("${").append(alias).append("}").append(path); final Parameter[] parameters = method.getParameters(); for (int i = 0; i < parameters.length; i++) { final Parameter parameter = parameters[i]; for (Annotation annotation : parameter.getAnnotations()) { if (!checkValidParams(annotation, args)) { break; } if (annotation instanceof RequestParam) { final String argName = ((RequestParam) annotation).value(); if (argName.isEmpty()) { throw new ConfigurationException("Configuration error: defined RequestParam annotation dont have value! Api method: " + method.getName() + ", Api Class: " + type); } final String toString = url.toString(); if (toString.endsWith("&") && i + 1 == args.length) { url.append(argName).append("=").append("${").append(argName).append("}"); } else if (!toString.endsWith("&") && i + 1 == args.length) { url.append("?").append(argName).append("=").append("${").append(argName).append("}"); } else if (!toString.endsWith("&")) { url.append("?").append(argName).append("=").append("${").append(argName).append("}").append("&"); } else { url.append(argName).append("=").append("${").append(argName).append("}").append("&"); } } else if (annotation instanceof PathVariable) { final String argName = ((PathVariable) annotation).value(); if (argName.isEmpty()) { throw new ConfigurationException("Configuration error: defined PathVariable annotation dont have value! Api method: " + method.getName() + ", Api Class: " + type); } final String toString = url.toString(); final String argStr = String.format("${%s}", argName); if (!toString.contains(argStr)) { if (toString.endsWith("/")) { url.append(argStr); } else { url.append("/").append(argStr); } } } else if (annotation instanceof RequestHeader) { final String argName = ((RequestHeader) annotation).value(); if (argName.isEmpty()) { throw new ConfigurationException("Configuration error: defined RequestHeader annotation dont have value! Api method: " + method.getName() + ", Api Class: " + type); } httpHeaders.add(argName, String.format("${%s}", argName)); } } } if (defaultHeaders.length > 0) { Arrays.stream(defaultHeaders) .forEach(header -> httpHeaders.add(header.name(), header.value())); } final Object instance = serviceMapping.fallbackClass().newInstance(); fallbackInstances.add(instance); final String fallbackName = serviceMapping.fallbackMethod(); final String buildedUrl = url.toString(); final MappingMetadata mappingMetadata = new MappingMetadata(method, httpMethod, buildedUrl, httpHeaders, fallbackName); metadatas.add(mappingMetadata); log.info("Bind api path - alias: {}, url: {}", alias, buildedUrl); } final BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(MappingFactoryBean.class); beanDefinitionBuilder.addPropertyValue("type", className); beanDefinitionBuilder.addPropertyValue("alias", alias); beanDefinitionBuilder.addPropertyValue("metadatas", metadatas); beanDefinitionBuilder.addPropertyValue("fallbackInstances", fallbackInstances); final AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition(); final BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{type.getSimpleName()}); BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); } catch (IllegalAccessException | InstantiationException | ClassNotFoundException | ConfigurationException e) { e.printStackTrace(); } } private boolean checkValidParams(Annotation annotation, Object[] args) { Arrays .stream(args) .map(Object::getClass) .forEach(type -> { if (annotation instanceof RequestParam) { if (type.isAnnotationPresent(PathVariable.class)) { throw new IllegalArgumentException("Annotation RequestParam cannot be used with PathVariable"); } } else if (annotation instanceof PathVariable) { if (type.isAnnotationPresent(RequestParam.class)) { throw new IllegalArgumentException("Annotation PathVariable cannot be used with RequestParam"); } } }); return true; } private Set<String> getBasePackages(AnnotationMetadata importingClassMetadata) { Map<String, Object> attributes = importingClassMetadata.getAnnotationAttributes(SpringBootApplication.class.getCanonicalName()); if (attributes == null) { attributes = importingClassMetadata.getAnnotationAttributes(ComponentScan.class.getCanonicalName()); } Set<String> basePackages = new HashSet<>(); if (attributes != null) { basePackages = Arrays.stream((String[]) attributes.get("scanBasePackages")).filter(StringUtils::hasText).collect(Collectors.toSet()); Arrays.stream((Class[]) attributes.get("scanBasePackageClasses")).map(ClassUtils::getPackageName).forEach(basePackages::add); } if (basePackages.isEmpty()) { basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName())); } return basePackages; } private ClassPathScanningCandidateComponentProvider getScanner() { return new ClassPathScanningCandidateComponentProvider(false, environment) { @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { boolean isCandidate = false; if (beanDefinition.getMetadata().isIndependent()) { if (!beanDefinition.getMetadata().isAnnotation()) { isCandidate = true; } } return isCandidate; } }; } }
即 在应用程序开始时发生的事情-触发Spring REFRESH上下文事件时,将涉及所有导入到应用程序上下文中的ImportBeanDefinitionRegistrar接口实现,并且将调用registerBeanDefinitions方法,该方法将接收有关带注释的配置类和bean(组件,组件的工厂注册/存储)的信息。服务,存储库等),然后通过这种方法,您可以获得有关基本应用程序包的信息,以及“挖掘方式”以搜索我们的接口并使用进行初始化 BeanDefinitionBulder和我们的MappingFactoryBean实现的强大功能。 要导入注册服务商,只需使用具有此类名称的Import批注就足够了(在模块的当前实现中,使用RestClientAutoConfiguration配置类,在其中编写必要的批注以使模块正常工作)。
使用方法
案例-我们想从某个GitHub用户存储库中获取信息列表。
1)编写用于使用服务的配置(application.yml)
services: routes: - host: https://api.github.com # GitHub alias: github-service # , Mapping
1)实现与服务交互的界面
@Mapping(alias = "github-service")
2)服务电话
@SprinBootApplication public class RestApp { public static void main(String... args) { final ConfigurableApplicationContext context = SpringApplication.run(RestApp.class, args); final RestGateway restGateway = context.getType(RestGateway.class); final Mono<ArrayList> response = restGateway.getRepos("gencloud"); response.doOnSuccess(list -> log.info("Received response: {}", list)).subscribe(); } }
作为在调试中执行的结果,您可以看到这一点(为方便起见,您可以将得到的json响应的对象包装代替ArrayList类型;代码不同,因为它在带有反应堆测试库的隔离专区中使用了单元测试,但原理没有改变):

结论
当然,并不是每个人都喜欢这种方法,复杂的调试,在错误的位置戳了注释-从ConfigurationException耳中打了一个巴掌,写了一些配置,哦...
我将接受开发API的建设性愿望和建议,希望本文对阅读有所帮助。 谢谢您的关注。
所有代码都在这里。