欢迎查看上一篇博客:SpringCloud专题之一:Eureka

OpenFeign是一种声明式的webservice客户端调用框架。你只需要声明接口和一些简单的注解,就能像使用普通的Bean一样调用远程服务,Ribbon 和 OpenFeign 都可以实现服务调用和实现负载均衡.OpenFeign 也内置了Ribbon.

OpenFeign是在feign的基础上做了一些修改封装,增加了对Spring Mvc注解的支持.

OpenFiegn注解讲解

一般我们会使用@GetMapping和@PostMapping两种方式来调用Rest服务。

使用@RequestParam和@RequestBody来获取参数

@RequestBody只能用在Post请求中,并且一个Post请求只能有一个@RequestBody,@RequestBody的参数可以包含复杂类型。

@RequestParam可以用在Post和Get请求中,但是要注意,@RequestParam的参数只能是基本类型或者是Enum,或者是List和Map(List和Map里只能是基本类型),所以@RequestParam可以和@RequestBody一起使用。

如果是Get请求,但是有时复合类型怎么办呢?比如我们想传递一个User对象,User对象里面只有普通的两个String属性,就可以使用@SpringQueryMap。@SpringQueryMap的参数是鞥你是普通的POJO,不能是复合类型,否则解析不了,如果必须使用复合类型,那么使用@RequestBody吧。

多个FeignClient使用同一个name的问题

在eureka的客户端添加一个类:

/**
* @className: Hello1Controller
* @description: 测试多个feign使用相同的name的问题
* @author: charon
* @create: 2021-06-06 09:35
*/
@RestController
public class Hello1Controller {
/**
* 日志记录类
*/
private final Logger logger = LoggerFactory.getLogger(getClass()); @Value("${server.port}")
private String host; @Value("${spring.application.name}")
private String instanceName; @RequestMapping("/sayHello1")
public String sayHello1(@RequestParam("name") String name){
logger.info("你好,服务名:{},端口为:{},接收到的参数为:{}",instanceName,host,name);
return "你好,服务名:"+instanceName+",端口为:"+host+",接收到的参数为:"+name;
}
}

eureka消费者端的controller里添加sayHello1(String name)方法:

/**
* @className: CustomerController
* @description:
* @author: charon
* @create: 2021-05-19 22:56
*/
@RestController
public class CustomerController { @Autowired
private CustomerSerivce serivce; @RequestMapping("/sayHello")
public String invokeSayHello(){
return serivce.invokeSayHello();
} @RequestMapping("/sayHello1")
public String invokeSayHello1(String name){
return serivce.invokeSayHello1(name);
}
}

接口及实现类:

/**
* @className: CustomerSerivce
* @description:
* @author: charon
* @create: 2021-05-19 22:56
*/
public interface CustomerSerivce { String invokeSayHello(); String invokeSayHello1(String name);
} @Service
public class CustomerServiceImpl implements CustomerSerivce { @Autowired
private CustomerFeign feign; @Autowired
private Customer1Feign feign1; @Override
public String invokeSayHello() {
return feign.sayHello();
} @Override
public String invokeSayHello1(String name) {
return feign1.sayHello1(name);
}
}

feignClient:

/**
* @className: CustomerFeign
* @description: @FeignClient使用的value参数,表示从HELLO-SERVER这个服务中调用服务
* @author: charon
* @create: 2021-05-19 23:01
*/
@FeignClient("HELLO-SERVER")
public interface CustomerFeign { /**
* 要求:
* 返回值要对应,方法名随意,参数值要对应
* 方法上添加SpringMVC的注解
* @return
*/
@RequestMapping("/sayHello")
String sayHello();
} /**
* @className: Customer1Feign
* @description: 测试多个feign使用相同的name的问题
* @author: charon
* @create: 2021-06-06 09:42
*/
@FeignClient("HELLO-SERVER")
public interface Customer1Feign {
/**
* 要求:
* 必须要指定RequestParam属性的value值,同时RequestMethod的method也需要指定
* 方法上添加SpringMVC的注解
* @return
*/
@RequestMapping(value = "/sayHello1",method = RequestMethod.GET)
String sayHello1(@RequestParam("name") String name);
}

如上图所示,运行时候就会报错。 原因是两个FeignClient使用了同一个value,对于同一个service-id只能使用一个配置类,如果有多个@FeignClient注解使用了相同的name属性,则注解的configuration参数会被覆盖。至于谁覆盖谁要看Spring容器初始化Bean的顺序。

改动:

# 设置为true,表示后发现的bean会覆盖之前相同名称的bean
spring.main.allow-bean-definition-overriding=true

源码解读

openfeign的自动配置

@EnableFeignClients开启openfeign

首先,我们从@EnableFeignClients这个注解开始了解。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}

这个注解导入了一个类FeignClientsRegistrar,这个类实现了ImportBeanDefinitionRegistrar接口,该接口用于向Bean容器中注册添加BeanDefinition。

跟进FeignClientsRegistrar的registerBeanDefinitions方法,看看它注册了哪些东西。

public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 注册默认配置,会读取@EnableFeignClients接口的属性,如果存在自定义配置类那么就会被注册到容器中
registerDefaultConfiguration(metadata, registry);
// 注册FeignClient接口的Bean,会扫描所有注解了@FeignClient的接口,然后像spring本地Bean一样地注册到容器中。
registerFeignClients(metadata, registry);
}

下面重点看看,registerFeignClients方法,这个方法的核心逻辑就是扫描类路径,获取BeanDefinition,然后遍历进行注册。

public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {

    LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
if (clients == null || clients.length == 0) {
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
// 扫描所有路径,默认情况下扫描启动类下的路径
Set<String> basePackages = getBasePackages(metadata);
for (String basePackage : basePackages) {
// 将所有 @FeignClient 的接口的BeanDefinition拿到
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
}
else {
for (Class<?> clazz : clients) {
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
}
}
// 遍历扫描到的FeignClient的Bean
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface"); Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName()); String name = getClientName(attributes);
// 注册FeignClient的配置
registerClientConfiguration(registry, name,attributes.get("configuration"));
// 注册FeignClient
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}

下面来看看注册FeignClient的方法:

private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
Class clazz = ClassUtils.resolveClassName(className, null);
ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
? (ConfigurableBeanFactory) registry : null;
String contextId = getContextId(beanFactory, attributes);
String name = getName(attributes);
FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
factoryBean.setBeanFactory(beanFactory);
factoryBean.setName(name);
factoryBean.setContextId(contextId);
factoryBean.setType(clazz);
// 使用FactoryBean,将Bean的具体生成过程收拢到FeignClientFactoryBean之中
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(clazz, () -> {
factoryBean.setUrl(getUrl(beanFactory, attributes));
factoryBean.setPath(getPath(beanFactory, attributes));
factoryBean.setDecode404(Boolean
.parseBoolean(String.valueOf(attributes.get("decode404"))));
Object fallback = attributes.get("fallback");
if (fallback != null) {
factoryBean.setFallback(fallback instanceof Class
? (Class<?>) fallback
: ClassUtils.resolveClassName(fallback.toString(), null));
}
Object fallbackFactory = attributes.get("fallbackFactory");
if (fallbackFactory != null) {
factoryBean.setFallbackFactory(fallbackFactory instanceof Class
? (Class<?>) fallbackFactory
: ClassUtils.resolveClassName(fallbackFactory.toString(),
null));
}
return factoryBean.getObject();
});
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
definition.setLazyInit(true);
validate(attributes); AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean); // has a default, won't be null
boolean primary = (Boolean) attributes.get("primary"); beanDefinition.setPrimary(primary); String[] qualifiers = getQualifiers(attributes);
if (ObjectUtils.isEmpty(qualifiers)) {
qualifiers = new String[] { contextId + "FeignClient" };
} BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
// 将这个使用了 @FeignClient 的接口的工厂Bean的 BeanDefinition 注册到Spring容器中
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

这里值得注意的是genericBeanDefinition方法最终生成的其实是FeignClientFactoryBean,而registerBeanDefinition方法注册进容器的也是FeignClientFactoryBean。而FeignClientFactoryBean是FactoryBean的实现类。FactoryBean接口是spring开放出来的,用于自定义Bean的生成过程。也就是说,spring将会通过调用FeignClientFactoryBean的getObject来获取@FeignClient注解的接口对应的Bean对象。

openfeign生成并调用客户端动态代理对象

从FeignClientFactoryBean的getObject()方法开始,看看代理对象的生成。getObject()方法调用了一个getTarget()方法,该方法做了一些预处理。获取了一个上下文以及Feign的构造器,没有URL的情况下拼接了一个。

@Override
public Object getObject() {
return getTarget();
} <T> T getTarget() {
// 获取上下文,FeignContext是在FeignAutoConfiguration被解析的时候成为Bean.
FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
: applicationContext.getBean(FeignContext.class);
// feign用于构建代理对象,builder将会构建feign
Feign.Builder builder = feign(context); if (!StringUtils.hasText(url)) {
// 然后在没有url 的情况下是按照服务名进行处理,拼接url 属性为http://服务名称。
// 如果有URL会按照URL的方式进行处理,并且如果URL没有加http:// 会在这里加上,也就是URL可以只写域名加端口
if (!name.startsWith("http")) {
url = "http://" + name;
}
else {
url = name;
}
url += cleanPath();
// HardCodedTarget 对象,实际上就是一个记录的功能,记录了接口类型,服务名称,地址信息
return (T) loadBalance(builder, context,
new HardCodedTarget<>(type, name, url));
}
if (StringUtils.hasText(url) && !url.startsWith("http")) {
url = "http://" + url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
client = ((LoadBalancerFeignClient) client).getDelegate();
}
if (client instanceof FeignBlockingLoadBalancerClient) {
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
client = ((RetryableFeignBlockingLoadBalancerClient) client)
.getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(type, name, url));
}

用org.springframework.cloud.openfeign.FeignClientFactoryBean#loadBalance 方法:

protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
// 获取执行HTTP请求的client对象
Client client = getOptional(context, Client.class);
if (client != null) {
builder.client(client);
// 获取Target对象,默认为HystrixTargeter
Targeter targeter = get(context, Targeter.class);
// 创建代理对象
return targeter.target(this, builder, context, target);
}
}

跟进HystrixTargeter的target方法:

@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
FeignContext context, Target.HardCodedTarget<T> target) {
if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
return feign.target(target);
}
feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
String name = StringUtils.isEmpty(factory.getContextId()) ? factory.getName()
: factory.getContextId();
SetterFactory setterFactory = getOptional(name, context, SetterFactory.class);
if (setterFactory != null) {
builder.setterFactory(setterFactory);
}
Class<?> fallback = factory.getFallback();
if (fallback != void.class) {
return targetWithFallback(name, context, target, builder, fallback);
}
Class<?> fallbackFactory = factory.getFallbackFactory();
if (fallbackFactory != void.class) {
return targetWithFallbackFactory(name, context, target, builder,
fallbackFactory);
} return feign.target(target);
}

HystrixTargeter的target方法里,最后调用了feign.target(target);方法,feign实现了构造代理对象的过程,所以这里将会回调feign的构造过程方法,在feign的target方法中,将会构造出一个Feign对象,并返回对象。

public <T> T target(Target<T> target) {
return build().newInstance(target);
} public Feign build() {
// ...
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}

跟进ReflectiveFeign#newInstance方法,主要是通过JDK的动态代理构建代理对象:

public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
} else if (Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler); for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}

代理对象的构建主要由3个内容组成:

  1. 构建Method到MethodHandler的映射关系,后面调用代理对象的时候将会根据Method找到MethodHandler,然后调用MethodHandler的invoke方法,而MethodHandler包含发起HTTP请求的实现。
  2. jdk动态代理需要提供InvocationHandler。而InvocationHandler将由InvocationHandlerFactory的create方法实现。
  3. 通过Proxy.newProxyInstance方法,生成proxy对象。

调用proxy对象发起HTTP请求

我们都知道,JDK的动态代理将会调用FeignInvocationHandler(ReflectiveFeign的静态内部类)的invoke方法.

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// equals、toString、hashcode方法特殊处理
return dispatch.get(method).invoke(args);
}

在前面构建代理对象的时候,构建了Method到MethodHandler的映射关系.所以在这里就是根据method来获取到MethodHandler,在调用invoke方法的.

进入到invoke方法里,MethodHandler接口的默认实现类为SynchronousMethodHandler:

@Override
public Object invoke(Object[] argv) throws Throwable {
// 根据上面创建对象过程中解析出来的RequestTemplate克隆一个RequestTemplate
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
// executeAndDecode将会负责发起http请求
return executeAndDecode(template, options);
} catch (RetryableException e) {
try {
retryer.continueOrPropagate(e);
} catch (RetryableException th) {
// ...
}
continue;
}
}
}
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
// 对FeignInteceptor 拦截器做处理,并将信息封装到feign.Request 类中
Request request = targetRequest(template); if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
} Response response;
long start = System.nanoTime();
try {
// 执行HTTP请求
response = client.execute(request, options);
// ensure the request is set. TODO: remove in Feign 12
response = response.toBuilder()
.request(request)
.requestTemplate(template)
.build();
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
}
throw errorExecuting(request, e);
}
long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); // 解码结果
if (decoder != null)
return decoder.decode(response, metadata.returnType()); CompletableFuture<Object> resultFuture = new CompletableFuture<>();
asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response,
metadata.returnType(),
elapsedTime); try {
if (!resultFuture.isDone())
throw new IllegalStateException("Response handling not done"); return resultFuture.join();
} catch (CompletionException e) {
Throwable cause = e.getCause();
if (cause != null)
throw cause;
throw e;
}
}

总结

openFeign生成@FeignClient注解的接口的代理对象是从FeignClientFactoryBean的getObject方法开始的,生成proxy对象主要由ReflectiveFeign对象来实现。动态代理方法由jdk原生的动态代理支持。

调用proxy对象,其实就是发起http请求,请求结果将被解码并返回。

所以,正如Feign本身的意义一样,http远程调用被伪装成了本地调用一样简单的代理对象,对于使用者来说就是调用本地接口一样简单

参考文章:

https://zhuanlan.zhihu.com/p/133378040

https://blog.csdn.net/manzhizhen/article/details/110013311

https://www.cnblogs.com/qlqwjy/p/14568086.html

Spring Cloud专题之二:OpenFeign的更多相关文章

  1. Spring Cloud专题之三:Hystrix

    在微服务架构中,我们将系统拆分成很多个服务单元,各单位的应用间通过服务注册与订阅的方式相互依赖.由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问 ...

  2. Spring Cloud 专题之四:Zuul网关

    书接上回: SpringCloud专题之一:Eureka Spring Cloud专题之二:OpenFeign Spring Cloud专题之三:Hystrix 经过前面三章对Spring Cloud ...

  3. Spring Cloud专题之五:config

    书接上回: SpringCloud专题之一:Eureka Spring Cloud专题之二:OpenFeign Spring Cloud专题之三:Hystrix Spring Cloud 专题之四:Z ...

  4. Spring Cloud 专题之六:bus

    书接上回: SpringCloud专题之一:Eureka Spring Cloud专题之二:OpenFeign Spring Cloud专题之三:Hystrix Spring Cloud 专题之四:Z ...

  5. Spring Cloud 专题之七:Sleuth 服务跟踪

    书接上回: SpringCloud专题之一:Eureka Spring Cloud专题之二:OpenFeign Spring Cloud专题之三:Hystrix Spring Cloud 专题之四:Z ...

  6. Spring Cloud(十二):分布式链路跟踪 Sleuth 与 Zipkin【Finchley 版】

    Spring Cloud(十二):分布式链路跟踪 Sleuth 与 Zipkin[Finchley 版]  发表于 2018-04-24 |  随着业务发展,系统拆分导致系统调用链路愈发复杂一个前端请 ...

  7. spring cloud: Hystrix(二):简单使用@HystrixCommand的commandProperties配置@HistrixProperty隔离策略

    spring cloud: Hystrix(二):简单使用@HystrixCommand的commandProperties配置@HistrixProperty隔离策略 某电子商务网站在一个黑色星期五 ...

  8. Spring Cloud Alibaba(二) 配置中心多项目、多配置文件、分目录实现

    介绍 之前Spring Cloud Config基础篇这篇文章介绍了Spring Cloud Config 配置中心基础的实现,今天继续聊下Spring Cloud Config 并结合nacos做服 ...

  9. spring cloud 专题二(spring cloud 入门搭建 之 微服务搭建和注册)

    一.前言 本文为spring cloud 微服务框架专题的第二篇,主要讲解如何快速搭建微服务以及如何注册. 本文理论不多,主要是傻瓜式的环境搭建,适合新手快速入门. 为了更好的懂得原理,大家可以下载& ...

随机推荐

  1. AZscaaner源码解读之数据库连接(一)

    准备开个新坑,但是可能近期不会更新,先写一篇开个头. sqlalchemy 目前在Python中使用得比较多的是sqlalchemy,sqlalchemy是一个对象关系映射(ORM).sqlalche ...

  2. PHP 调用请求外网接口

    1.类中定义静态方法 class FtpService{ /** * 请求外网 * @param $url 外网接口url * @param bool $params 参数,拼接字符串 post请求可 ...

  3. 检查dtd和Xschema文件限制下的xml文件是否符合的Java文件

    先来xml文件: 1 <?xml version="1.0" encoding="utf-8"?> 2 <!DOCTYPE orders SY ...

  4. Java常用类详解

    目录 1. String类 1.1 String的特性 1.2 String字面量赋值的内存理解 1.3 String new方式赋值的内存理解 1.4 String 拼接字面量和变量的方式赋值 1. ...

  5. Nios II系统在Quartus II编译后Timing requirements for slow timing model timing analysis were not met. See Report window for details

    来自http://wenku.baidu.com/link?url=h0Z_KvXD3vRAn9H8mjfbVErVOF_Kd3h-BZSyF1r4sEYj3ydJGEfBHGY1mvntP4HDuF ...

  6. ssh-的搭建和使用

    ssh的作用 : 可实现远程客户端登录服务器并对服务器的文件进行操作 ssh服务器的安装 farsight@ubuntu:~$ sudo apt-get install openssh-server ...

  7. 快速熟悉windows操作

    快捷键 win + E : 打开我的电脑 Ctrl+Shift+Esc:打开资源管理器 Alt +F4 :关闭当前窗口 Win + R:打开命令窗口 DOS 命令 打开CMD 的方式 Win+R:输入 ...

  8. [Python] 微信公众号开发 Python3

    搭建服务 开通一个阿里云ecs,安装python3及需要的包(参考下方官方文档) 将py文件保存在ecs上,运行 在本地访问阿里云的IP地址 能完成这步说明网络没问题 server.py 1 # -* ...

  9. [刷题] 46 Permutations

    要求 整型数组,每个元素不相同,返回元素所有排列的可能 示例 [1,2,3] [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ] 思路 树 ...

  10. Bash技巧:使用 set 内置命令帮助调试 shell 脚本

    Bash技巧:使用 set 内置命令帮助调试 shell 脚本 霜鱼片发布于 2020-02-03   在 bash 中,可以使用 set 内置命令设置和查看 shell 的属性.这些属性会影响 sh ...