• Spring Cloud Feign 总结


    Spring Cloud中, 服务又该如何调用 ?

    各个服务以HTTP接口形式暴露 , 各个服务底层以HTTP Client的方式进行互相访问。

    SpringCloud开发中,Feign是最方便,最为优雅的服务调用实现方式。

    Feign 是一个声明式,模板化的HTTP客户端,可以做到用HTTP请求访问远程服务就像调用本地方法一样。简单搭建步骤如下 :

    1. 首先加入pom.xml

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    

    2. 主类上面添加注解@EnableFeignClients,该注解表示当程序启动时,会进行包扫描,默认扫描所有带@FeignClient注解的类进行处理

    package name.ealen;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    import org.springframework.cloud.netflix.feign.EnableFeignClients;
    
    /**
     * Created by EalenXie on 2018/10/12 18:24.
     */
    @SpringBootApplication
    @EnableFeignClients
    @EnableDiscoveryClient
    public class FeignOpenClientApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(FeignOpenClientApplication.class,args);
        }
    }
    

    3. 简单配置appliation.yml 注册到Eureka Server。

    server:
      port: 8090
    spring:
      application:
        name: spring-cloud-feign-openClient
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:8761/eureka/
    

    4. 使用@FeignClient为本应用声明一个简单的能调用的客户端。为了方便,找个现成的开放接口,比如Github开放的api,GET /search/repositories。

    GitHub接口文档 : https://developer.github.com/v3/search/#search-repositories

    package name.ealen.client;
    
    import org.springframework.cloud.netflix.feign.FeignClient;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RequestParam;
    
    /**
     * Created by EalenXie on 2019/1/9 11:28.
     */
    @FeignClient(name = "github-client", url = "https://api.github.com")
    public interface GitHubApiClient {
    
        @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
        String searchRepositories(@RequestParam("q") String queryStr);
    }
    

    其中,@FeignClient 即是指定客户端信息注解,务必声明在接口上面,url手动指定了客户端的接口地址。

    5. 为其写一个简单Controller进行一波测试 :

    package name.ealen.web;
    
    import name.ealen.client.GitHubApiClient;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.annotation.Resource;
    
    /**
     * Created by EalenXie on 2018/10/12 18:32.
     */
    @RestController
    public class FeignOpenClientController {
    
        @Resource
        private GitHubApiClient gitHubApiClient;
    
        @RequestMapping("/search/github/repository")
        public String searchGithubRepositoryByName(@RequestParam("name") String repositoryName) {
            return gitHubApiClient.searchRepositories(repositoryName);
        }
    }
    

    6. 依次启动Eureka Server,和该应用。然后访问 : http://localhost:8090/search/github/repository?name=spring-cloud-dubbo

    注 : 有时候在测试的时候,很容易报500 null的异常,可能是因为GitHub连接拒绝的原因,这里只是为了测试,所以可以忽略,多尝试几次即可。

    关于Feign Client配置细节

    1. 重点配置 @FeignClient 注解,我这里专门对源码属性做了说明 :

    在上例中,我们只是简单的指定了name和url属性,如果需要专门针对该客户端进行属性按需调整,可以调整以下参数 属性值 :

    package org.springframework.cloud.netflix.feign;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    import org.springframework.core.annotation.AliasFor;
    
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface FeignClient {
    
    	@AliasFor("name")
    	String value() default "";  
    	
    	@Deprecated
    	String serviceId() default "";
    	
    	@AliasFor("value")
    	String name() default "";
    
    	String qualifier() default "";
    
    	String url() default "";
    
    	boolean decode404() default false;
    
    	Class<?>[] configuration() default {};
        
    	Class<?> fallback() default void.class;
    	
    	Class<?> fallbackFactory() default void.class;
    
    	String path() default "";
        
    	boolean primary() default true;
    }
    
    name:               指定Feign Client的名称,如果项目使用了 Ribbon,name属性会作为微服务的名称,用于服务发现。
    serviceId:          用serviceId做服务发现已经被废弃,所以不推荐使用该配置。
    value:              指定Feign Client的serviceId,如果项目使用了 Ribbon,将使用serviceId用于服务发现,但上面可以看到serviceId做服务发现已经被废弃,所以也不推荐使用该配置。
    qualifier:          为Feign Client 新增注解@Qualifier
    url:                请求地址的绝对URL,或者解析的主机名
    decode404:          调用该feign client发生了常见的404错误时,是否调用decoder进行解码异常信息返回,否则抛出FeignException。
    fallback:           定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback 指定的类必须实现@FeignClient标记的接口。实现的法方法即对应接口的容错处理逻辑。
    fallbackFactory:    工厂类,用于生成fallback 类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码。
    path:               定义当前FeignClient的所有方法映射加统一前缀。
    primary:            是否将此Feign代理标记为一个Primary Bean,默认为ture
    

    例如我们要为其添加一个fallback的容错,和覆盖掉默认的configuration。

    1. 首先为其添加一个fallback容错的处理类.
    package name.ealen.client;
    
    /**
     * Created by EalenXie on 2018/11/11 19:19.
     */
    public class GitHubApiClientFallBack implements GitHubApiClient {
    
        @Override
        public String searchRepositories(String queryStr) {
            return "call github api fail";
        }
    }
    
    2. 然后为其添加一个默认配置类,为了方便了解,我这里只是写了一下默认的配置。
    package name.ealen.config;
    
    import feign.Contract;
    import feign.Logger;
    import feign.Retryer;
    import feign.codec.Decoder;
    import feign.codec.Encoder;
    import feign.codec.ErrorDecoder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class GitHubFeignConfiguration {
        /**
         * Feign 客户端的日志记录,默认级别为NONE
         * Logger.Level 的具体级别如下:
         * NONE:不记录任何信息
         * BASIC:仅记录请求方法、URL以及响应状态码和执行时间
         * HEADERS:除了记录 BASIC级别的信息外,还会记录请求和响应的头信息
         * FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据
         */
        @Bean
        Logger.Level gitHubFeignLoggerLevel() {
            return Logger.Level.FULL;
        }
    }
    

    注意 : 编码器,解码器,重试器,调用解析器请谨慎配置,一般来说默认就行。笔者对这些配置研究得很浅,所以没有写自定义的配置。

    3. 此时我们修改我们的GitHubApiClient。指定上面两个类即可
    package name.ealen.client;
    import name.ealen.config.GitHubFeignConfiguration;
    import org.springframework.cloud.netflix.feign.FeignClient;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RequestParam;
    /**
     * Created by EalenXie on 2019/1/9 11:28.
     */
    @FeignClient(name = "github-client", url = "https://api.github.com", path = "", serviceId = "", qualifier = "", fallback = GitHubApiClientFallBack.class, decode404 = false, configuration = GitHubFeignConfiguration.class)
    public interface GitHubApiClient {
        @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
        String searchRepositories(@RequestParam("q") String queryStr);
    }
    
    4. Feign也支持属性文件对上面属性的配置,比如下面的配置和GitHubFeignConfiguration的配置是等价的 :
    feign:
      client:
        config:
          ##对名字为 github-client 的feign client做配置
          github-client:                                # 对应GitHubApiClient类的@FeignClient的name属性值
            decoder404: false                           # 是否解码404
            loggerLevel: full                           # 日志记录级别
    

    2. 重点配置 @EnableFeignClients 注解,我这里专门对源码属性做了说明 :

    package org.springframework.cloud.netflix.feign;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    import org.springframework.context.annotation.Import;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import(FeignClientsRegistrar.class)
    public @interface EnableFeignClients {
    
    	String[] value() default {};
    
    	String[] basePackages() default {};
    
    	Class<?>[] basePackageClasses() default {};
    
    	Class<?>[] defaultConfiguration() default {};
    
    	Class<?>[] clients() default {};
    }
    
    value:                  等价于basePackages属性,更简洁的方式
    basePackages:           指定多个包名进行扫描
    basePackageClasses:     指定多个类或接口的class,扫描时会在这些指定的类和接口所属的包进行扫描。
    defaultConfiguration:   为所有的Feign Client设置默认配置类
    clients:                指定用@FeignClient注释的类列表。如果该项配置不为空,则不会进行类路径扫描。
    

    同样的,为所有Feign Client 也支持文件属性的配置,如下 :

    feign:
      client:
        config:                                         
        # 默认为所有的feign client做配置(注意和上例github-client是同级的)
          default:                                      
            connectTimeout: 5000                        # 连接超时时间
            readTimeout: 5000                           # 读超时时间设置  
    

    注 : 如果通过Java代码进行了配置,又通过配置文件进行了配置,则配置文件的中的Feign配置会覆盖Java代码的配置。

    但也可以设置feign.client.defalult-to-properties=false,禁用掉feign配置文件的方式让Java配置生效。

    3. Feign 请求和响应开启GZIP压缩,提高通讯效率

    1. 配置如下:
    feign:
      compression:
        request:
          enable: true  #配置请求支持GZIP压缩,默认为false
          mime-types: text/xml, application/xml, application/json  #配置压缩支持的Mime Type
          min-request-size: 2048 #配置压缩数据大小的上下限
        reponse:
          enable: true #配置响应支持GZIP压缩,默认为false
    

    对应配置源码可以看看 :

    2. 由于开启GZIP压缩之后,Feign之间的调用通过二进制协议进行传输,返回的值需要修改为ResponseEntity<byte[]>才可以正常显示,否则会导致服务之间的调用结果乱码。
    3. 例如此时修改GitHubApiClient类的Feign client的配置 :
    import name.ealen.config.GitHubFeignConfiguration;
    import org.springframework.cloud.netflix.feign.FeignClient;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RequestParam;
    /**
     * Created by EalenXie on 2019/1/9 11:28.
     */
    @FeignClient(name = "github-client", url = "https://api.github.com", path = "", serviceId = "", qualifier = "", decode404 = false, configuration = GitHubFeignConfiguration.class)
    public interface GitHubApiClient {
    
    //    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
    //    Object searchRepositories(@RequestParam("q") String queryStr);
    
        @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
        ResponseEntity<byte[]> searchRepositories(@RequestParam("q") String queryStr);
    }
    

    4. Feign超时配置

    feign的调用分为两层。ribbon和hystrix(默认集成),默认情况下,hystrix是关闭的,所以当ribbon发生超时异常时,可以如下配置调整ribbon超时时间 :

    #ribbon的超时时间
    ribbon:
      ReadTimeout: 60000           # 请求处理的超时时间
      ConnectTimeout: 30000        # 请求连接的超时时间
    

    至于为什么这个配置会生效,我们可以大概看一下源码里面相关 键值对 的描述 :

    DefaultClientConfigImpl中 有许多的属性键配置 :

    CommonClientConfigKey :

    AbstractRibbonCommand 中的 ribbon 和 hystrix都用到的 getRibbonTimeout()方法 :

    默认情况下,feign中的hystrix是关闭的。

    如果开启了hystrix。此时的ribbon的超时时间和Hystrix的超时时间的结合就是Feign的超时时间,当hystrix发生了超时异常时,可以如下配置调整hystrix的超时时间 :

    feign:
      hystrix:
        enable: true
    hystrix:
      shareSecurityContext: true    # 设置这个值会自动配置一个Hystrix并发策略会把securityContext从主线程传输到你使用的Hystrix command
      command:
        default:
          execution:
            isolation:
              thread:
                timeoutInMillisecond: 10000   # hystrix超时时间调整 默认为1s
          circuitBreaker:
            sleepWindowInMilliseconds: 10000     # 短路多久以后开始尝试是否恢复,默认5s
            forceClosed: false # 是否允许熔断器忽略错误,默认false, 不开启
    

    关于HystrixCommandProperties类的以上配置说明,详细可以参阅 : https://www.jianshu.com/p/b9af028efebb

    注 : 当开启了Ribbon之后,可能会出现首次调用失败的情况。

    原因 : 因为hystrix的默认超时时间是1s,而feign首次的请求都会比较慢,如果feign的响应时间(ribbon响应时间)大于了1s,就会出现调用失败的问题。

    解决方法 :

    1. 将Hystrix的超时时间尽量修改得长一点。(有时候feign进行文件上传的时候,如果时间太短,可能文件还没有上传完就超时异常了,这个配置很有必要)
    2. 禁用Hystirx的超时时间 : hystrix.command.default.execution.timeout.enabled=false
    3. Feign直接禁用Hystrix(不推荐) : feign.hystrix.enabled=false
    

    Feign 的HTTP请求相关

    1. Feign 默认的请求 Client 替换

    feign在默认情况下使用JDK原生的URLConnection 发送HTTP请求。(没有连接池,保持长连接)

    1. 使用HTTP Client替换默认的Feign Client

    引入pom.xml :

    <!--Apache HttpClient 替换Feign原生的httpclient-->
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
    </dependency>
    

    配置application.yml支持httpclient :

    feign: 
      httpclient:
        enable: true
    
    2. 使用okhttp替换Feign默认的Client

    引入pom.xml :

    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-okhttp</artifactId>
    </dependency>
    

    配置application.yml支持okhttp :

    feign: 
      httpclient:
        enable: false
      okhttp:
        enable: true
    

    配置okhttp :

    import feign.Feign;
    import okhttp3.ConnectionPool;
    import org.springframework.boot.autoconfigure.AutoConfigureBefore;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    import org.springframework.cloud.netflix.feign.FeignAutoConfiguration;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.concurrent.TimeUnit;
    
    @Configuration
    @ConditionalOnClass(Feign.class)
    @AutoConfigureBefore(FeignAutoConfiguration.class)
    public class FeignOkHttpConfig {
        @Bean
        public okhttp3.OkHttpClient okHttpClient() {
            return new okhttp3.OkHttpClient.Builder()
                    .connectTimeout(60, TimeUnit.SECONDS)   //设置连接超时
                    .readTimeout(60, TimeUnit.SECONDS)      //设置读超时
                    .writeTimeout(60, TimeUnit.SECONDS)     //设置写超时
                    .retryOnConnectionFailure(true)                 //是否自动重连
                    .connectionPool(new ConnectionPool())           //构建OkHttpClient对象
                    .build();
        }
    }
    

    2. Feign的Get多参数传递

    Feign 默认不支持GET方法直接绑定POJO的,目前解决方式如下 :

    1. 把POJO拆散成一个个单独的属性放在方法参数里面;
    2. 把方法的参数变成Map传递;
    3. GET传递@RequestBody。(此方式违反了Restful规范,而且我们一般不会这样写)
    

    《重新定义Spring Cloud实战》一书中介绍了一种最佳实践方式,通过Feign的拦截器的方式进行处理。实现原理是通过Feign的RequestInterceptor中的apply方法,统一拦截转换处理Feign中的GET方法多参数。处理如下 :

    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import feign.RequestInterceptor;
    import feign.RequestTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    
    import java.io.IOException;
    import java.util.*;
    
    @Component
    public class FeignRequestInterceptor implements RequestInterceptor {
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        public void apply(RequestTemplate template) {
            //feign 不支持GET方法传POJO,json body 转query
            if (template.method().equals("GET") && template.body() != null) {
                try {
                    JsonNode jsonNode = objectMapper.readTree(template.body());
                    template.body(null);
                    Map<String, Collection<String>> queries = new HashMap<>();
                    buildQuery(jsonNode, "", queries);
                    template.queries(queries);
                } catch (IOException e) {
                    //提示:根据实践项目情况处理此处异常,这里不做扩展。
                    e.printStackTrace();
                }
            }
        }
    
        private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) {
            if (!jsonNode.isContainerNode()) {   // 叶子节点
                if (jsonNode.isNull()) return;
                Collection<String> values = queries.computeIfAbsent(path, k -> new ArrayList<>());
                values.add(jsonNode.asText());
                return;
            }
            if (jsonNode.isArray()) {   // 数组节点
                Iterator<JsonNode> it = jsonNode.elements();
                while (it.hasNext()) {
                    buildQuery(it.next(), path, queries);
                }
            } else {
                Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields();
                while (it.hasNext()) {
                    Map.Entry<String, JsonNode> entry = it.next();
                    if (StringUtils.hasText(path))
                        buildQuery(entry.getValue(), path + "." + entry.getKey(), queries);
                    else   // 根节点
                        buildQuery(entry.getValue(), entry.getKey(), queries);
                }
            }
        }
    }
    

    3. feign的文件上传

    1. 首先我们编写一个简单文件上传服务的应用,并将其注册到Eureka Server上面

    简单配置一下,application的name为feign-file-upload-application。为其写一个上传的接口 :

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.http.MediaType;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.io.File;
    
    @RestController
    public class FeignUploadController {
        private static final Logger log = LoggerFactory.getLogger(FeignUploadController.class);
        @PostMapping(value = "/server/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
        public String fileUploadServer(MultipartFile file) throws Exception {
            log.info("upload file name : {}", file.getName());
            //上传文件放到 /usr/temp/uploadFile/ 目录下
            file.transferTo(new File("/usr/temp/uploadFile/" + file.getName()));
            return file.getOriginalFilename();
        }
    }
    
    2. 编写一个要使用上传功能的feign 客户端 :

    feign客户端应用还需要加入依赖,pom.xml :

    <!-- Feign文件上传依赖-->
    <dependency>
        <groupId>io.github.openfeign.form</groupId>
        <artifactId>feign-form</artifactId>
        <version>3.0.3</version>
    </dependency>
    <dependency>
        <groupId>io.github.openfeign.form</groupId>
        <artifactId>feign-form-spring</artifactId>
        <version>3.0.3</version>
    </dependency>
    

    客户端指定接口信息 :

    import name.ealen.config.FeignMultipartSupportConfiguration;
    import org.springframework.cloud.netflix.feign.FeignClient;
    import org.springframework.http.MediaType;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RequestPart;
    import org.springframework.web.multipart.MultipartFile;
    
    @FeignClient(value = "feign-file-upload-application", configuration = FeignMultipartSupportConfiguration.class)
    public interface FileUploadFeignService {
        /***
         * 1.produces,consumes必填
         * 2.注意区分@RequestPart和RequestParam,不要将
         * : @RequestPart(value = "file") 写成@RequestParam(value = "file")
         */
        @RequestMapping(method = RequestMethod.POST, value = "/uploadFile/server", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
        public String fileUpload(@RequestPart(value = "file") MultipartFile file);
    }
    
    import feign.form.spring.SpringFormEncoder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.context.annotation.Scope;
    import feign.codec.Encoder;
    /**
     * Feign文件上传Configuration
     */
    @Configuration
    public class FeignMultipartSupportConfiguration {
        @Bean
        @Primary
        @Scope("prototype")
        public Encoder multipartFormEncoder() {
            return new SpringFormEncoder();
        }
    }
    

    注意 : 文件上传功能的feign client 与其他的feign client 配置要分开,因为用的是不同的Encoder和处理机制,以免互相干扰,导致请求抛Encoder不支持的异常。

    4. feign的调用传递headers里面的信息内容

    默认情况下,当通过Feign调用其他的服务时,Feign是不会带上当前请求的headers信息的。

    如果我们需要调用其他服务进行鉴权的时候,可能会需要从headers中获取鉴权信息。则可以通过实现Feign的拦截RequestInterceptor接口,进行获取headers。然后手动配置到feign请求的headers中去。

    import feign.RequestInterceptor;
    import feign.RequestTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.Enumeration;
    
    @Component
    public class FeignHeadersInterceptor implements RequestInterceptor {
        @Override
        public void apply(RequestTemplate template) {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            Enumeration<String> headerNames = request.getHeaderNames();
            if (headerNames != null) {
                while (headerNames.hasMoreElements()) {
                    String keys = headerNames.nextElement();
                    String values = request.getHeader(keys);
                    template.header(keys, values);
                }
            }
        }
    }
    
  • 相关阅读:
    springsecurity-微服务-springsecurity工具类封装
    springsecurity-微服务-认证授权的过程
    springsecurity-CSRF
    springsecurity-自动登录实现
    Vulnhub-靶机-DC: 9
    Xss-labs-level3-6
    靶机-生成自有账户和密码提权
    靶机-敲击相关方式汇总
    Vulnhub-靶机-DIGITALWORLD.LOCAL: BRAVERY
    Xss-labs-level1-2
  • 原文地址:https://www.cnblogs.com/ealenxie/p/10275794.html
Copyright © 2020-2023  润新知