OpenFegin+hystrix实现远程HTTP服务调用、服务降级(深度~保姆级)
简介
OpenFeign 是 Spring Cloud 家族的一个成员, 它最核心的作用是为 HTTP 形式的 Rest API 提供了非常简洁高效的 RPC 调用方式。支持Hystrix 、Ribbon和SpringMVC 注解。
Feign和OpenFeign的区别?
1、Feign:
Feign是Netflix公司(第一代SpringCloud)研发的一个轻量级RESTful的伪HTTP服务客户端。
Feign内置了Ribbon逻辑,通过负载均衡算法可以从注册中心中寻找服务。
2、OpenFeign:
OpenFeign是SpringCloud自己研发的,在Feign的基础上做了增强。
OpenFeign除了原有Ribbon逻辑外,还支持了Hystrix和Spring MVC注解。
一、服务端Provider
注意: 当前是springboot整合OpenFegin 服务端没有配置注册中心, 如分布式 微服务请自行配置
-
-
-
-
public class CrmCustomersController {
-
-
-
private CrmCustomersService crmCustomersService;
-
-
/**
-
* 服务数据
-
*/
-
-
public PmpResult queryCustomerList (int pageNum, int pageSize,String customersno, String customersname,String daogouid){
-
try {
-
-
PageInfo<CustomerInformationVo> pageInfo = crmCustomersService.queryCustomerList(pageNum,pageSize,customersno,customersname,daogouid);
-
-
logger.info("查询成功");
-
return PmpResult.success(pageInfo);
-
}catch (Exception e) {
-
logger.error(e.getMessage(), e);
-
String errorMessage = "查询异常";
-
if (isDev){
-
errorMessage = e.getMessage();
-
}
-
return PmpResult.paramError(errorMessage);
-
}
-
-
}
-
-
}
二、消费端Consumer
-
<!-- openFeing --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
@FeignClient注解
@FeignClient参数:
name: 指定FeignClient的(服务)名称,在Eureka/Nacos/其他注册中心 中对应的服务名称。
url: 指定远程调用地址。
configuration: 指定配置类,可以自定义FeignClient的配置。
fallback: 指定降级类,在服务调用失败时返回降级类中的内容。
fallbackFactory: 指定降级工厂类,在服务调用失败时返回降级工厂类中的内容。
decode404: 指定404响应是否解码,默认为true。
path: 指定服务请求的基础路径。
contextId: 当一个服务有多个接口时,我们又不想把所有接口都写到一个类中是时候就用到了 contextId为当前类设置一个唯一ID。不然就回报如下错误 。
-
[扩展]@FeignClient注解 通过属性 contextId 解决名称重复无法启动的问题 错误如下:
-
Description:
-
The bean 'optimization-user.FeignClientSpecification', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.
-
Action:
-
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
导致 名称重复问题 复现:
-
/*
-
*Consumer1
-
*/
-
-
public interface UserRemoteConsumer1 {
-
-
public User getUser(int id);
-
}
-
-
/*
-
*Consumer2
-
*/
-
-
public interface UserRemoteConsumer2 {
-
-
public User getUser(int id);
-
}
这种情况下启动就会报错了,因Bean的名称冲突。
原因:由于name重复,而又不允许BeanDefinition重复,所以导致在进行注册时报错。
解决方案一:
增加下面的配置,作用允许出现beanName一样的BeanDefinition。
spring.main.allow-bean-definition-overriding=true
解释:
spring.main.allow-bean-definition-overriding=true应用程序的一个配置属性,它允许在应用程序上下文中覆盖bean定义。如果设置为true,则可以在应用程序上下文中定义多个具有相同名称的bean,后定义的bean将覆盖先前定义的bean。但是,这可能会导致不可预测的行为和错误,因此应该谨慎使用。
解决方案二(推荐):
每个FeignClient手动指定不同的contextId,这样就不会冲突了。
@FeignClient添加contextId属性
-
/*
-
*Consumer1
-
*/
-
-
public interface UserRemoteConsumer1 {
-
-
public User getUser(int id);
-
}
-
-
/*
-
*Consumer2
-
*/
-
-
public interface UserRemoteConsumer2 {
-
-
public User getUser(int id);
-
}
总结:想创建多个具有相同名称或url的外部客户端,以便它们指向同一台服务器,但每个服务器都有不同的自定义配置,那么可以使用@FeignClient的contextId属性,以避免这些配置bean的名称冲突。
-
@RequestMapping(value = "/customer/information/customerlist", method = RequestMethod.GET)是要调的接口名
openFegin调用如下:
-
import com.alibaba.fastjson.JSONObject;
-
import com.lt.crm.service.fallback.TransactionFallBackFactory;
-
import org.springframework.cloud.openfeign.FeignClient;
-
import org.springframework.stereotype.Repository;
-
import org.springframework.web.bind.annotation.RequestMapping;
-
import org.springframework.web.bind.annotation.RequestMethod;
-
import org.springframework.web.bind.annotation.RequestParam;
-
-
-
-
-
public interface ConsumerService {
-
-
-
JSONObject queryCustomerList(final int pageNum, final int pageSize);
-
-
-
}
说明:
Hystrix配合OpenFeign进行降级,可以对应接口中定义的远程调用单独进行降级操作。
直白的说: 远程调用失败,我们添加一个替代方案,我们知道OpenFegin是以接口的形式来声明远程调用,只要是远程调用失效超时,就执行替代方案。创建一个实现类,对原有的接口方法进行替代方案实现。
-
使用@Component注解 交给String管理 注入IOC容器
-
对Api接口进行进行实现、重写调用的Api 远程接口 代码如下:
-
@Component 注解是 Spring 框架中的注解,用于将一个类标记为 Spring 容器中的一个组件,让 Spring 自动扫描并管理这个组件的生命周期和依赖注入。
-
实现implements FallbackFactory原因:用于在使用 Feign 进行服务调用时,定义一个 FallbackFactory<> 来处理服务调用失败的情况的<ConsumerService>。[ import feign.hystrix.FallbackFactory; ]
-
以下码为例:服务降级内容我只是返回一段话,根据自己业务定性, 可判断参数是否为空等 返回具体错误信息。
-
import com.alibaba.fastjson.JSONObject;
-
import com.lt.crm.service.api.ConsumerService;
-
import feign.hystrix.FallbackFactory;
-
import org.apache.commons.lang.exception.ExceptionUtils;
-
import org.slf4j.Logger;
-
import org.slf4j.LoggerFactory;
-
import org.springframework.stereotype.Component;
-
-
-
-
public class TransactionFallBackFactory implements FallbackFactory<ConsumerService> {
-
-
private Logger logger = LoggerFactory.getLogger(getClass());
-
-
public ConsumerService create(Throwable throwable) {
-
-
return new ConsumerService() {
-
-
public JSONObject queryCustomerList(int pageNum, int pageSize) {
-
logger.error("TransactionMsgWaitingConfirm Error : {}" , ExceptionUtils.getFullStackTrace(throwable));
-
String fallbackMessage = "被Hystrix熔断,已作降级处理!";
-
JSONObject jsonObject = new JSONObject();
-
jsonObject.put("fallbackMessage",fallbackMessage);
-
return jsonObject;
-
}
-
};
-
}
-
-
-
-
}
@Autowired
private ConsumerService consumerService;
在消费端Controller注入Api调用 ConsumerService
-
-
import com.alibaba.fastjson.JSONObject;
-
import com.lt.crm.common.PmpResult;
-
import com.lt.crm.service.api.ConsumerService;
-
import io.swagger.annotations.Api;
-
import io.swagger.annotations.ApiOperation;
-
import org.slf4j.Logger;
-
import org.slf4j.LoggerFactory;
-
import org.springframework.beans.factory.annotation.Autowired;
-
import org.springframework.stereotype.Controller;
-
import org.springframework.web.bind.annotation.RequestMapping;
-
import org.springframework.web.bind.annotation.RequestMethod;
-
import org.springframework.web.bind.annotation.RequestParam;
-
import org.springframework.web.bind.annotation.ResponseBody;
-
-
-
-
-
-
public class ConsumerController {
-
/**
-
* 日志
-
*/
-
private final static Logger logger = (Logger) LoggerFactory.getLogger(ConsumerController.class);
-
-
-
private ConsumerService consumerService;
-
-
-
-
-
public PmpResult queryCustomerList(int pageNum, int pageSize) {
-
-
try {
-
// String类型 转换成json 接收
-
JSONObject GetJsonObject = consumerService.queryCustomerList(pageNum, pageSize);
-
if (null != GetJsonObject) {
-
logger.info("调用数据成功");
-
return PmpResult.success("数据服务", GetJsonObject);
-
}
-
-
} catch (Exception e) {
-
e.printStackTrace();
-
return PmpResult.serviceError("服务异常");
-
}
-
return null;
-
}
-
-
-
}
说明: OpenFegin 默认1ms 未响应会抛出异常 , 往往在调用远程接口的时候 、业务逻辑复杂、 没等业务处理完毕就抛出异常 、我们可以针对这类问题 可以配置延长时长。
-
配置 Feign 超时问题
-
配置hystrix 超时时间
-
######################################openFegin######################################
-
# 默认开启
-
feign.httpclient.enabled=true
-
# 默认关闭
-
feign.okhttp.enabled=false
-
# 默认关闭
-
feign.hystrix.enabled=true
-
# 默认关闭
-
feign.sentinel.enabled=false
-
-
# default context 连接超时时间 5000ms = 5秒
-
feign.client.config.default.connectTimeout = 5000
-
# default context 读超时时间
-
feign.client.config.default.readTimeout = 5000
-
-
######################################Hystrix Config######################################
-
#开启hystrix超时管理
-
hystrix.command.default.execution.timeout.enabled=true
-
#hystrix超时时间
-
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=60000
-
-
#开启ribbon超时管理
-
ribbon.http.client.enabled=false
-
#请求超时时间
-
ribbon.ReadTimeout=20000
-
#连接超时时间
-
ribbon.ConnectTimeout=20000
-
ribbon.MaxAutoRetries=0
-
ribbon.MaxAutoRetriesNextServer=1
-
ribbon.OkToRetryOnAllOperations=false
启动类上使用注解 @EnableFeignClients 开启 openFeign 功能。
-
/**
-
* 微服务应用服务启动类
-
* 1、(@EnableDiscoveryClient)注解为链接微服务注册中心用,如实际环境中使用注册中心,请取消注释部分,
-
* 与配置文件中相关注册中心配置信息结合使用。
-
* @author xj
-
*
-
*/
-
-
//加入@EnableFeignClients注解 开启Feign远程服务调用,使Feign的bean可以被注入
-
-
-
-
public class OpenFeignApplication {
-
public static void main(String[] args) {
-
SpringApplication.run(OpenFeignApplication.class, args);}
-
}
启动消费端Consumer,并没有启动服务端Provider ,过了超时时间,此时执行了服务降级,这样做避免了在微服务中服务雪崩的灾难。
后台也捕获了error :
连接被拒绝:连接执行GET: Connection refused: connect executing GET http://10.1.8.22:9001/customer/information/customerlist?pageNum=1&pageSize=10
-
2023-03-17 16:56:52.399 ERROR 20928 --- [ervice-client-1] c.l.c.s.f.TransactionFallBackFactory : TransactionMsgWaitingConfirm Error : feign.RetryableException: Connection refused: connect executing GET http://10.1.8.22:9001/customer/information/customerlist?pageNum=1&pageSize=10
-
at feign.FeignException.errorExecuting(FeignException.java:84)
-
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:113)
-
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:78)
-
at feign.hystrix.HystrixInvocationHandler$1.run(HystrixInvocationHandler.java:106)
-
at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:302)
-
at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:298)
-
at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:46)
-
at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:35)
-
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48)
-
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
-
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48)
-
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
-
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48)
-
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
-
at rx.Observable.unsafeSubscribe(Observable.java:10327)
-
at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:51)
-
at rx.internal.operators.OnSubscribeDefer.call(OnSubscribeDefer.java:35)
-
at rx.Observable.unsafeSubscribe(Observable.java:10327)
-
at rx.internal.operators.OnSubscribeDoOnEach.call(OnSubscribeDoOnEach.java:41)
-
at rx.internal.operators.OnSubscribeDoOnEach.call(OnSubscribeDoOnEach.java:30)
-
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:48)
-
at rx.internal.operators.OnSubscribeLift.call(OnSubscribeLift.java:30)
-
at rx.Observable.unsafeSubscribe(Observable.java:10327)
-
at rx.internal.operators.OperatorSubscribeOn$SubscribeOnSubscriber.call(OperatorSubscribeOn.java:100)
-
at com.netflix.hystrix.strategy.concurrency.HystrixContexSchedulerAction$1.call(HystrixContexSchedulerAction.java:56)
-
at com.netflix.hystrix.strategy.concurrency.HystrixContexSchedulerAction$1.call(HystrixContexSchedulerAction.java:47)
-
at com.netflix.hystrix.strategy.concurrency.HystrixContexSchedulerAction.call(HystrixContexSchedulerAction.java:69)
-
at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55)
-
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
-
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
-
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
-
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
-
at java.lang.Thread.run(Thread.java:748)
现在启动服务端再次调用,已经通过消费者9004端口 拿到了服务端9001提供的数据。
三、openFeign传参方式
简介:
在接口当中传参的方式有很多,但是在 openFeig中的 传参是 有一定规则的,详细如下。
服务端Provider接口中JSON传参方法如下:
-
-
-
public class OpenFeignProviderController {
-
-
public Order createOrder1({ Order order)
-
return order;
-
}
-
}
注意:在Spring Boot 中通过@RequestBody标识入参。
消费端Consumer在openFeign接口传参如下:
-
//服务名
-
public interface OpenFeignService {
-
/**
-
* 参数默认是@RequestBody标注的,这里的@RequestBody可以不填
-
* 方法名称任意
-
*/
-
-
Order createOrder1(; Order Order)
-
}
注意: 我用的是@RequestMapping注解 这是一个组合注解,里面包含@RequestBody 可以不填。
服务端Provider接口传参方法如下:
-
-
-
public class OpenFeignProviderController {
-
-
public Order createOrder2(Order order){
-
return order;
-
}
-
}
注意: 参数使用POJO对象接收。
消费端Consumer在openFeign接口传参如下:
-
-
public interface OpenFeignService {
-
/**
-
* 参数默认是@RequestBody标注的,如果通过POJO表单传参的,使用@SpringQueryMap标注
-
*/
-
-
Order createOrder2(; Order order)
-
}
注意: openFeign提供了一个注解@SpringQueryMap解决POJO表单传参,这也是官方文档明确给出了解决方案。
服务端Provider接口传参方法如下:
-
-
-
public class OpenFeignProviderController {
-
-
-
public String test({ Integer id)
-
return "accept one msg id=" id;
-
}
注意: 此种方式针对restful方式中的GET请求方式。
消费端Consumer在openFeign接口传参如下:
-
-
public interface OpenFeignService {
-
-
-
String get(; Integer id)
-
}
注意: 使用注解@PathVariable接收url中的占位符。
四、开启日志增强
说明: openFeign 虽提供了日志增强功能,但默认是不显示任何日志的,开发者在调试阶段可以自己配置日志的级别。
级别如下:
-
NONE:默认的,不显示任何日志;
-
BASIC:仅记录请求方法、URL、响应状态码及执行时间;
-
HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息;
-
FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。
自定义一个配置类,在其中设置日志级别如下:
-
-
import feign.Logger;
-
import org.springframework.context.annotation.Bean;
-
import org.springframework.context.annotation.Configuration;
-
-
-
public class OpenFeginConfig {
-
/**
-
* 日志级别定义
-
* @return
-
*/
-
-
Logger.Level OpenFeignLoggerLevel(){
-
return Logger.Level.FULL;
-
}
-
}
注意细节:Logger导包是feign包。
-
logging:
-
level:
-
com.lt.crm.service.api: debug
注意:此处 com.lt.crm.service.api 是 openFeign 接口所在的包名,可以根据自己项目路径配置一个特定的openFeign接口。
说明:@FeignClient配置里 configuration = OpenFeginConfig.class
-
-
-
public interface ConsumerService {
-
-
-
JSONObject queryCustomerList(final int pageNum, final int pageSize);
-
-
-
}
五、Gzip压缩数据(全局压缩~接口与浏览器响应压缩)
-
######################################接口与浏览器响应压缩######################################
-
#全局压缩(接口与浏览器响应压缩)
-
server:
-
compression:
-
#开启Gzip压缩
-
enabled: true
-
#配置min-response-size 压缩数据大小的最小阈值,默认2048
-
min-response-size: 1 #设置1kb以上开始压缩
-
#配置mime-types 压缩支持的MIME TYPE
-
mime-types:
-
- image/png
-
- image/jpeg
-
- image/jpg
-
- text/html
-
- text/xml
-
- application/xml
-
- application/json
min-response-size 和 mime-types 含默认值,可以不手动指定,如有其他需要可按需指定
未开启压缩前 请求数据大小为526KB
开启压缩 请求数据大小为82KB
点击查看请求头信息
Gzip是一种数据格式,采用deflate算法压缩数据;当GZIP算法压缩到一个纯文本数据时,效果会非常显著。
数据经过压缩后实际上降低了网络传输的字节数,最明显的好处就是可以加快网页加载的速度。网页加载速度加快,除了节省流量,改善用户的浏览体验外。
Gzip压缩传输原理图如下:
1、客户端向服务器请求头中带有:Accept-Encoding:gzip,deflate 字段,向服务器表示,客户端支持的压缩格式(gzip或者deflate),如果不发送该消息头,服务器是不会压缩的。
2、服务端在收到请求之后,如果发现请求头中含有 Accept-Encoding 字段,并且支持该类型的压缩,就对响应报文压缩之后返回给客户端,并且携带 Content-Encoding:gzip 消息头,表示响应报文是根据该格式压缩过的。
3、客户端接收到响应之后,先判断是否有 Content-Encoding 消息头,如果有,按该格式解压报文。否则按正常报文处理。
六、OpenFeign开启Gzip压缩数据(局部压缩~微服务之间利用Feign请求及响应压缩)
-
######################################openFegin Gzip######################################
-
#局部压缩(微服务之间利用feign请求及响应压缩)
-
feign:
-
compression:
-
request:
-
enabled: true # 开启压缩
-
min-request-size: 1 # 开启压缩的阈值,单位字节,默认2048,即是2k,此处为演示效果设置成1字节
-
mime-types: text/xml,application/xml,application/json
-
response:
-
enabled: true #响应压缩
注意:openFeign支持的Gzip仅仅是在openFeign接口的请求和响应,即openFeign消费者调用服务提供者的接口。
查看Gzip压缩开启后的打印日志
日志中包含 content-encoding: gzip 表示压缩成功
查看Gzip压缩关闭后的打印日志
日志中未出现 content-encoding: gzip 表示未被压缩 并且可以清楚看到时间为 1231ms
七、如何OpenFegin 替换默认的httpclient ?
前言:
1、OpenFeign在默认情况下使用的是JDK原生的URLConnection发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,即利用HTTP的persistence connection。
2、默认的http客户端是 javax.net.ssl.HttpsURLConnection,详细信息查看feign-core:feign.Client,该http客户端不支持添加拦截器和连接池。所以我们需要添加第三方http客户端。
在生产环境中,通常也不会使用默认的httpclient,两种选择如下:
-
使用ApacheHttpClient
-
使用OkHttp
声明: 我使用的是 OkHttp 如果使用 ApacheHttpClient 思路是基本一致 。
-
<!-- 使用 Apache HttpClient 替换 Feign原生httpclient-->
-
<dependency>
-
<groupId>org.apache.httpcomponents</groupId>
-
<artifactId>httpclient</artifactId>
-
</dependency>
-
<!-- 使用 Okhttp 替换 Feign原生httpclient-->
-
<dependency>
-
<groupId>io.github.openfeign</groupId>
-
<artifactId>feign-okhttp</artifactId>
-
<version>10.1.0</version>
-
</dependency>
-
# 默认开启
-
feign.httpclient.enabled=false
-
# 默认关闭
-
feign.okhttp.enabled=true
关闭默认的httpclient 开启okhttp
前言:
微服务之间使用OpenFeign,由于业务得需求有时候我们需要在请求或者响应的时候做一些额外的操作。比如请求的时候添加请求头,响应时候判断token是否过期等等。这时候拦截器就派上用场了!
-
import lombok.extern.slf4j.Slf4j;
-
import okhttp3.Interceptor;
-
import okhttp3.MediaType;
-
import okhttp3.OkHttpClient;
-
import okhttp3.Request;
-
import okhttp3.Response;
-
import okhttp3.ResponseBody;
-
import org.springframework.context.annotation.Bean;
-
import org.springframework.context.annotation.Configuration;
-
import java.io.IOException;
-
-
-
-
public class FeignOkHttpClientConfig {
-
-
-
-
public OkHttpClient.Builder okHttpClientBuilder() {
-
return new OkHttpClient.Builder().addInterceptor(new FeignOkHttpClientResponseInterceptor());
-
}
-
-
-
/**
-
* okHttp响应拦截器
-
*/
-
public static class FeignOkHttpClientResponseInterceptor implements Interceptor {
-
-
-
-
public Response intercept(Chain chain) throws IOException {
-
-
Request originalRequest = chain.request();
-
Response response = chain.proceed(originalRequest);
-
-
MediaType mediaType = response.body().contentType();
-
String content = response.body().string();
-
//解析content,做你想做的事情!!!
-
System.out.print("拦截器" content "往哪跑?");
-
-
//生成新的response返回,网络请求的response如果取出之后,直接返回将会抛出异常
-
return response.newBuilder()
-
.body(ResponseBody.create(mediaType, content))
-
.build();
-
}
-
}
-
-
-
-
}
问题复现:
-
2023-03-27 16:43:23.754 ERROR 21120 --- [ervice-client-1] c.l.c.s.f.TransactionFallBackFactory : TransactionMsgWaitingConfirm Error : java.lang.IllegalStateException: original request is required
-
at feign.Util.checkState(Util.java:127)
-
at feign.Response.<init>(Response.java:48)
-
at feign.Response.<init>(Response.java:38)
-
at feign.Response$Builder.build(Response.java:133)
-
at feign.okhttp.OkHttpClient.toFeignResponse(OkHttpClient.java:99)
-
at feign.okhttp.OkHttpClient.execute(OkHttpClient.java:161)
具体描述:
可以看到状态200 请求成功 ,并且我设置的超时是5秒,而我查看后台日志请求只用了130ms说明没有超时,为什么一直走熔断? 为什么会一直报: java.lang.IllegalStateException: original request is required
解决方案:
最初我用的okhttp 版本是9.7.0 导致,feign-core 的版本和 feign-okhttp版本不一致问题引起,或者可以理解版本为不兼容导致,将 feign-okhttp版本换成 10.1.0 成方可解决。
这篇好文章是转载于:学新通技术网
- 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
- 本站站名: 学新通技术网
- 本文地址: /boutique/detail/tanhfhkefj
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
excel下划线不显示怎么办
PHP中文网 06-23 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
怎样阻止微信小程序自动打开
PHP中文网 06-13 -
photoshop蒙版画笔没反应怎么办
PHP中文网 06-24