拥抱UniHttp,规范Http接口对接之旅

news2024/9/9 0:28:35

前言

如果你项目里还在用传统的编程式Http客户端比如HttpClient、Okhttp去直接对接第三方Http接口,
那么你项目一定充斥着大量的对接逻辑和代码, 并且针对不同的对接渠道方需要每次封装一次调用的简化,
一旦封装不好系统将会变得难以维护,难以阅读, 甚至不同的开发同学会用自己的方式用不同的Http客户端用不同的封装逻辑去对接接口,
这种情况一般发生于项目换了维护者,然后代码负责人也没把控代码质量和规范。

如果你的项目里也存在这样的问题,那么UniHttp就是你的规范你的版本答案。

1、简介

一个声明式的Http接口对接框架,能以极快的方式完成对一个第三方Http接口的对接和使用,只要配置一下即可重复使用,
不需要开发者去关注如何发送一个请求,如何去传递Http请求参数,以及如何对请求结果进行处理和反序列化,这些框架都帮你一一实现
就像配置 Spring的Controller 那样简单,只不过相当于是反向配置而已

该框架更注重于如何保持高内聚和可读性高的代码情况下与快速第三方渠道接口进行对接和集成,而非像传统编程式的Http请求客户端(比如HttpClient、Okhttp)那样专注于如何去发送Http请求,虽然底层也是用的Okhttp去发送请求。 与其说的是对接的Http接口,不如说是对接的第三方渠道,UniHttp可支持自定义接口渠道方HttpAPI注解以及一些自定义的对接和交互行为 ,为此扩展了发送和响应和反序列化一个Http请求的各种生命周期钩子,开发者可自行去扩展实现。

2、快速开始

2.1、引入依赖

    <dependency>
      <groupId>io.github.burukeyou</groupId>
      <artifactId>uniapi-http</artifactId>
      <version>0.0.4</version>
    </dependency>

2.2、对接接口

在类上标记@HttpApi注解,然后指定请求的域名url, 然后就可以为方法配发去对接哪个接口。

比如下面两个方法的配置则对接了 GET http://localhost:8080/getUser和 POST http://localhost:8080/addUser 两个接口

方法返回值定义成Http响应body对应的类型即可,默认会使用fastjson反序列化Http响应body的值为该类型对象。

@HttpApi(url = "http://localhost:8080")
interface UserHttpApi {
    
   @GetHttpInterface("/getUser")
   BaseRsp<String> getUser(@QueryPar("name") String param,@HeaderPar("userId") Integer id);
    
   @PostHttpInterface("/addUser")
   BaseRsp<Add4DTO> addUser(@BodyJsonPar Add4DTO req);
   
}

@QueryPar 表示将参数值放到Http请求的查询参数内

@HeaderPar 表示将参数值放到Http请求的请求头里

@BodyJsonPar 表示将参数值放到Http请求body内,并且content-type是application/json

1、getUser方法最终构建的Http请求报文为

GET http://localhost:8080/getUser?name=param
Header:
    userId: id

2、addUser最终构建的Http请求报文为

        POST:  http://localhost:8080/addUser 
        Header: 
            Content-Type:   application/json
        Body:
            {"id":1,"name":"jay"}

2.3、声明定义的HttpAPI的包扫描路径

在spring的配置类上使用@UniAPIScan注解标记定义的HttpAPI的包扫描路径,会自动为标记了@HttpApi接口生成代理对象并且注入到Spring容器中,
之后只需要像使用Spring的其他bean一样,依赖注入使用即可

@UniAPIScan("com.xxx.demo.api")
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class,args);
    }
}

2.4 依赖注入使用即可

@Service
class UserAppService {
    
    @Autowired
    private UserHttpApi userHttpApi;
    
    public void doSomething(){
        userHttpApi.getUser("jay",3);
    }
} 

3、说明介绍

3.1、@HttpApi注解

用于标记接口上,该接口上的方法会被代理到对应的Http请求接口,可指定请求的域名,也可指定自定义的Http代理逻辑等等。

3.2、@HttpInterface注解

用于配置一个接口的参数,包括请求方式、请求路径、请求头、请求cookie、请求查询参数等等

并且内置了以下请求方式的@HttpInterface,不必再每次手动指定请求方式

  • @PostHttpInterface
  • @PutHttpInterface
  • @DeleteHttpInterface
  • @GetHttpInterface
    @PostHttpInterface(
            // 请求路径
            path = "/getUser",
            // 请求头
            headers = {"clientType:sys-app","userId:99"},
            // url查询参数 
            params = {"name=周杰伦","age=1"},
            // url查询参数拼接字符串
            paramStr = "a=1&b=2&c=3&d=哈哈&e=%E7%89%9B%E9%80%BC",
            // cookie 字符串
            cookie = "name=1;sessionId=999"
    )
    BaseRsp<String> getUser();

3.3、@Par注解

以下各种Par后缀的注解,主要用于方法参数上,用于指定在发送请求时将参数值放到Http请求体的哪部分上。

为了方便描述,下文描述的普通值就是表示String,基本类型、基本类型的包装类型等类型.

简单复习下Http协议报文
在这里插入图片描述

@QueryPar注解

标记Http请求url的查询参数

支持以下方法参数类型的标记: 普通值、普通值集合、对象、Map

    @PostHttpInterface
    BaseRsp<String> getUser(@QueryPar("id")  String id,  //  普通值   
                            @QueryPar("ids") List<Integer> idsList, //  普通值集合
                            @QueryPar User user,  // 对象
                            @QueryPar Map<String,Object> map); // Map

    

如果类型是普通值或者普通值集合需要手动指定参数名,因为是当成单个查询参数传递

如果类型是对象或者Map是当成多个查询参数传递,字段名或者map的key名就是参数名,字段值或者map的value值就是参数值。

  • 如果是对象,参数名默认是字段名,由于用的是fastjson序列化可以用@JSONField指定别名

@PathPar注解

标记Http请求路径变量参数,仅支持标记普通值类型

    @PostHttpInterface("/getUser/{userId}/detail")
    BaseRsp<String> getUser(@PathPar("userId")  String id);  //  普通值

@HeaderPar注解

标记Http请求头参数

支持以下方法参数类型: 对象、Map、普通值

    @PostHttpInterface
    BaseRsp<String> getUser(@HeaderPar("id")  String id,  //  普通值   
                            @HeaderPar User user,  // 对象
                            @HeaderPar Map<String,Object> map); // Map

    

如果类型是普通值类型需要手动指定参数名,当成单个请求头参数传递. 如果是对象或者Map当成多个请求头参数。

@CookiePar注解

用于标记Http请求的cookie请求头

支持以下方法参数类型: Map、Cookie对象、字符串

    @PostHttpInterface
    BaseRsp<String> getUser(@CookiePar("id")  String cookiePar,  //   普通值 (指定name)当成单个cookie键值对处理
                            @CookiePar String cookieString,  //  普通值 (不指定name),当成完整的cookie字符串处理
                            @CookiePar com.burukeyou.uniapi.http.support.Cookie cookieObj,  // 单个Cookie对象 
                            @CookiePar List<com.burukeyou.uniapi.http.support.Cookie> cookieList // Cookie对象列表
                            @CookiePar Map<String,Object> map); // Map

    

如果类型是字符串时,当指定参数名时,当成单个cookie键值对处理,如果不指定参数名时当成完整的cookie字符串处理比如a=1;b=2;c=3 这样

如果是Map当成多个cookie键值对处理。

如果类型是内置的 com.burukeyou.uniapi.http.support.Cookie对象当成单个cookie键值对处理

@BodyJsonPar注解

用于标记Http请求体内容为json形式: 对应content-type为 application/json

支持以下方法参数类型: 对象、对象集合、Map、普通值、普通值集合

    @PostHttpInterface
    BaseRsp<String> getUser(@BodyJsonPar  String id,                //  普通值
                            @BodyJsonPar  String[] id               //  普通值集合
                            @BodyJsonPar List<User> userList,       // 对象集合
                            @BodyJsonPar User user,                  // 对象
                            @BodyJsonPar Map<String,Object> map);    // Map

序列化和反序列化默认用的是fastjson,所以如果想指定别名,可以在字段上标记 @JSONField 注解取别名

@BodyFormPar注解

用于标记Http请求体内容为普通表单形式: 对应content-type为 application/x-www-form-urlencoded

支持以下方法参数类型: 对象、Map、普通值

    @PostHttpInterface
    BaseRsp<String> getUser(@BodyFormPar("name") String value,         //  普通值
                            @BodyFormPar User user,                   // 对象
                            @BodyFormPar Map<String,Object> map);    // Map

如果类型是普通值类型需要手动指定参数名,当成单个请求表单键值对传递

BodyMultiPartPar注解

用于标记Http请求体内容为复杂形式: 对应content-type为 multipart/form-data

支持以下方法参数类型: 对象、Map、普通值、File对象

    @PostHttpInterface
    BaseRsp<String> getUser(@BodyMultiPartPar("name") String value,         //  单个表单文本值
                            @BodyMultiPartPar User user,                   // 对象
                            @BodyMultiPartPar Map<String,Object> map,      // Map
                            @BodyMultiPartPar("userImg") File file);     // 单个表单文件值

如果参数类型是普通值或者File类型,当成单个表单键值对处理,需要手动指定参数名。

如果参数类型是对象或者Map,当成多个表单键值对处理。 如果字段值或者map的value参数值是File类型,则自动当成是文件表单字段传递处理

@BodyBinaryPar注解

用于标记Http请求体内容为二进制形式: 对应content-type为 application/octet-stream

支持以下方法参数类型: InputStream、File、InputStreamSource

    @PostHttpInterface
    BaseRsp<String> getUser(@BodyBinaryPar InputStream value,         
                            @BodyBinaryPar File user,                   
                            @BodyBinaryPar InputStreamSource map);    

@ComposePar注解

这个注解本身不是对Http请求内容的配置,仅用于标记一个对象,然后会对该对象内的所有标记了其他@Par注解的字段进行嵌套解析处理,
目的是减少方法参数数量,支持都内聚到一起传递

支持以下方法参数类型: 对象

    @PostHttpInterface
    BaseRsp<String> getUser(@ComposePar UserReq req);    

比如UserReq里面的字段可以嵌套标记其他@Par注解,具体支持的标记类型和处理逻辑与前面一致

class UserReq {

    @QueryPar
    private Long id;

    @HeaderPar
    private String name;

    @BodyJsonPar
    private Add4DTO req;

    @CookiePar
    private String cook;
}

3.4、原始的HttpResponse

HttpResponse表示Http请求的原始响应对象,如果业务需要关注拿到完整的Http响应,只需要在方法返回值包装返回即可。
如下面所示,此时HttpResponse<Add4DTO>里的泛型Add4DTO才是代表接口实际返回的响应内容,后续可直接手动获取

    @PostHttpInterface("/user-web/get")
    HttpResponse<Add4DTO> get();

通过它我们就可以拿到响应的Http状态码、响应头、响应cookie等等,当然也可以拿到我们的响应body的内容通过getBodyResult方法

3.5、处理文件下载接口

对于若是下载文件的类型的接口,可将方法返回值定义为 HttpBinaryResponse、HttpFileResponse、HttpInputStreamResponse 的任意一种,
这样就可以拿到下载后的文件。

  • HttpBinaryResponse: 表示下载的文件内容以二进制形式返回,如果是大文件请谨慎处理,因为会存放在内存中

  • HttpFileResponse: 表示下载的文件内容以File对象返回,这时文件已经被下载到了本地磁盘

  • HttpInputStreamResponse: 表示下载的文件内容输入流的形式返回,这时文件其实还没被下载到客户端,调用者可以自行读取该输入流进行文件的下载

3.6、HttpApiProcessor 生命周期钩子

HttpApiProcessor是一个Http请求接口的各种生命周期钩子,开发者可以实现它在里面自定义编写各种对接逻辑。 然后可以配置到@HttpApi注解或者@HttpInterface注解上, 然后框架内部默认会从SpringContext获取,获取不到则手动new一个。

  • 通常一个Http请求需要经历 构建请求参数、发送Http请求时,Http响应后获取响应内容、反序列化Http响应内容成具体对象。

目前提供了4种钩子,执行顺序流程如下:


                  postBeforeHttpMetadata                (请求发送前)在发送请求之前,对Http请求体后置处理
                         |
                         V
                  postSendingHttpRequest                (请求发送时)在Http请求发送时处理
                         |
                         V
               postAfterHttpResponseBodyString          (请求响应后)对响应body文本字符串进行后置处理
                         |
                         V
              postAfterHttpResponseBodyResult           (请求响应后)对响应body反序列化后的结果进行后置处理
                         |
                         V
              postAfterMethodReturnValue                (请求响应后)对代理的方法的返回值进行后置处理,类似aop的后置处理

.

1、postBeforeHttpMetadata: 可在发送http请求之前对请求体进行二次处理,比如加签之类

2、postSendHttpRequest: Http请求发送时会回调该方法,可以在该方法执行自定义的发送逻辑或者打印发送日志

3、postAfterHttpResponseBodyString: Http请求响应后,对响应body字符串进行进行后置处理,比如如果是加密数据可以进行解密

4、postAfterHttpResponseBodyResult: Http请求响应后,对响应body反序列化后的对象进行后置处理,比如填充默认返回值

5、postAfterMethodReturnValue: Http请求响应后,对代理的方法的返回值进行后置处理,类似aop的后置处理

.

回调参数说明:

  • HttpMetadata: 表示此次Http请求的请求体,包含请求url,请求头、请求方式、请求cookie、请求体、请求参数等等。

  • HttpApiMethodInvocation: 继承自MethodInvocation, 表示被代理的方法调用上下文,可以拿到被代理的类,被代理的方法,被代理的HttpAPI注解、HttpInterface注解等信息

3.7、配置自定义的Http客户端

默认使用的是Okhttp客户端,如果要重新配置Okhttp客户端,注入spring的bean即可,如下

@Configuration
public class CusotmConfiguration {

    @Bean
    public OkHttpClient myOHttpClient(){
        return new OkHttpClient.Builder()
                .readTimeout(50, TimeUnit.SECONDS)
                .writeTimeout(50, TimeUnit.SECONDS)
                .connectTimeout(10, TimeUnit.SECONDS)
                .connectionPool(new ConnectionPool(20,10, TimeUnit.MINUTES))
                .build();
    }
}

4、企业级渠道对接实战

案例背景:

  • 假设现在需要对接一个某天气服务的所有接口,需要在请求cookie带上一个token字段和sessionId字段, 这两个字段的值需要每次接口调用前先手动调渠道方的一个特定的接口申请获取,token值在该接口返回值中返回,sessionId在该接口的响应头中返回。然后还需要在请求头上带上一个sign签名字段, 该sign签名字段生成规则需要用渠道方提供的公钥对所有请求体和请求参数进行加签生成。
    然后还需要在每个接口的查询参数上都带上一个渠道方分配的客户端appId。

4.1 在application.yml中配置对接渠道方的信息

channel:
  mtuan:
    # 请求域名
    url: http://127.0.0.1:8999
    # 分配的渠道appId
    appId: UUU-asd-01
    # 分配的公钥
    publicKey: fajdkf9492304jklfahqq

4.2 自定义该渠道方的HttpAPI注解

假设现在对接的是某团,所以自定义注解叫@MTuanHttpApi吧,然后需要在该注解上标记@HttpApi注解,并且需要配置processor字段,需要去自定义实现一个HttpApiProcessor这个具体实现后续讲。

有了这个注解后就可以自定义该注解与对接渠道方相关的各种字段配置,当然也可以不定义。 注意这里url的字段是使用 @AliasFor(annotation = HttpApi.class),
这样构建的HttpMetadata中会默认解析填充要请求体,不标记则也可自行处理。

@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@HttpApi(processor = MTuanHttpApiProcessor.class)
public @interface MTuanHttpApi {

    /**
     * 渠道方域名地址
     */
    @AliasFor(annotation = HttpApi.class)
    String url() default "${channel.mtuan.url}";

    /**
     * 渠道方分配的appId
     */
    String appId() default "${channel.mtuan.appId}";
}

@Slf4j
@Component
public class MTuanHttpApiProcessor implements HttpApiProcessor<MTuanHttpApi> {
    
}

注意实现的HttpApiProcessor泛型要指定为刚才定义的注解@MTuanHttpApi类型,因为这个HttpApiProcessor配置到它上面,如果需要通用处理可以定义为Annocation类型

4.3 对接接口

有了@MTuanHttpApi注解之后就可以开始对接接口了,比如假设有两个接口要对接。一个就是前面说的获取令牌的接口。 一个是获取天气情况的接口。

  • 为什么getToken方法返回值是 HttpResponse, 这是UniHttp内置的原始Http响应对象,方便我们去拿到原始Http响应体的一些内容(比如响应状态码、响应cookie)。
    其中的泛型BaseRsp才是实际的Http响应体反序列化后的内容。 而getCityWeather方法没有使用HttpResponse包装,
    BaseRsp只是单纯Http响应体反序列化后的内容,这是两者的区别。 前面介绍过 HttpResponse,其实大部份接口是不关注HttpResponse的可以不用去配置。
@MTuanHttpApi
public interface WeatherApi {
    
    /**
     * 根据城市名获取天气情况
     */
    @GetHttpInterface("/getCityByName")
    BaseRsp<WeatherDTO> getCityWeather(@QueryPar("city") String cityName);

    /**
     *  根据appId和公钥获取令牌
     */
    @PostHttpInterface("/getToken")
    HttpResponse<BaseRsp<TokenDTO>> getToken(@HeaderPar("appId") String appId, @HeaderPar("publicKey")String publicKey);

}

4.4、自定义HttpApiProcessor

在之前我们自定义了一个@MTuanHttpApi注解上指定了一个MTuanHttpApiProcessor,接下来我们去实现他的具体内容为了实现我们案例背景里描述的功能。

@Slf4j
@Component
public class MTuanHttpApiProcessor implements HttpApiProcessor<MTuanHttpApi> {

    /**
     *  渠道方分配的公钥
     */
    @Value("${channel.mtuan.publicKey}")
    private String publicKey;

    @Value("${channel.mtuan.appId}")
    private String appId;

    @Autowired
    private Environment environment;
    
    @Autowired
    private WeatherApi weatherApi;

    /** 实现-postBeforeHttpMetadata: 发送Http请求之前会回调该方法,可对Http请求体的内容进行二次处理
     *
     * @param httpMetadata              原来的请求体
     * @param methodInvocation          被代理的方法
     * @return                          新的请求体
     */
    @Override
    public HttpMetadata postBeforeHttpMetadata(HttpMetadata httpMetadata, HttpApiMethodInvocation<MTuanHttpApi> methodInvocation) {
        /**
         * 在查询参数中添加提供的appId字段
         */
        // 获取MTuanHttpApi注解
        MTuanHttpApi apiAnnotation = methodInvocation.getProxyApiAnnotation();

        // 获取MTuanHttpApi注解的appId,由于该appId是环境变量所以我们从environment中解析取出来
        String appIdVar = apiAnnotation.appId();
        appIdVar = environment.resolvePlaceholders(appIdVar);

        // 添加到查询参数中
        httpMetadata.putQueryParam("appId",appIdVar);

        /**
         *  生成签名sign字段
         */
        // 获取所有查询参数
        Map<String, Object> queryParam = httpMetadata.getHttpUrl().getQueryParam();

        // 获取请求体参数
        HttpBody body = httpMetadata.getBody();

        // 生成签名
        String signKey = createSignKey(queryParam,body);

        // 将签名添加到请求头中
        httpMetadata.putHeader("sign",signKey);

        return httpMetadata;
    }

    private String createSignKey(Map<String, Object> queryParam, HttpBody body) {
        // todo 伪代码
        // 1、将查询参数拼接成字符串
        String queryParamString = queryParam.entrySet()
                .stream().map(e -> e.getKey() + "="+e.getValue())
                .collect(Collectors.joining(";"));

        // 2、将请求体参数拼接成字符串
        String bodyString = "";
        if (body instanceof HttpBodyJSON){
            // application/json  类型的请求体
            bodyString = body.toStringBody();
        }else if (body instanceof HttpBodyFormData){
            // application/x-www-form-urlencoded 类型的请求体
            bodyString = body.toStringBody();
        }else if (body instanceof HttpBodyMultipart){
            // multipart/form-data 类型的请求体
            bodyString =  body.toStringBody();
        }

        // 使用公钥publicKey 加密拼接起来
        String sign = publicKey + queryParamString + bodyString;
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] digest = md.digest(sign.getBytes());
            return new String(digest);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     *  实现-postBeforeHttpMetadata: 发送Http请求时,可定义发送请求的行为 或者打印请求和响应日志。
     */
    @Override
    public HttpResponse<?> postSendHttpRequest(HttpSender httpSender, HttpMetadata httpMetadata) {
        //  忽略 weatherApi.getToken的方法回调,否则该方法也会回调此方法会递归死循环。 或者该接口指定自定义的HttpApiProcessor重写postSendingHttpRequest
        Method getTokenMethod = ReflectionUtils.findMethod(WeatherServiceApi.class, "getToken",String.class,String.class);
        if (getTokenMethod == null || getTokenMethod.equals(methodInvocation.getMethod())){
            return httpSender.sendHttpRequest(httpMetadata);
        }
        
        // 1、动态获取token和sessionId
        HttpResponse<String> httpResponse = weatherApi.getToken(appId, publicKey);

        // 从响应体获取令牌token
        String token = httpResponse.getBodyResult();
        // 从响应头中获取sessionId
        String sessionId = httpResponse.getHeader("sessionId");

        // 把这两个值放到此次的请求cookie中
        httpMetadata.addCookie(new Cookie("token",token));
        httpMetadata.addCookie(new Cookie("sessionId",sessionId));
        
        log.info("开始发送Http请求 请求接口:{} 请求体:{}",httpMetadata.getHttpUrl().toUrl(),httpMetadata.toHttpProtocol());

        // 使用框架内置工具实现发送请求
        HttpResponse<?> rsp =  httpSender.sendHttpRequest(httpMetadata);

        log.info("开始发送Http请求 响应结果:{}",rsp.toHttpProtocol());
        
        return rsp;
    }

    /**
     *  实现-postAfterHttpResponseBodyResult: 反序列化后Http响应体的内容后回调,可对该结果进行二次处理返回
     * @param bodyResult                     Http响应体反序列化后的结果
     * @param rsp                            原始Http响应对象
     * @param method                         被代理的方法
     * @param httpMetadata                   Http请求体
     */
    @Override
    public Object postAfterHttpResponseBodyResult(Object bodyResult, HttpResponse<?> rsp, Method method, HttpMetadata httpMetadata) {
        if (bodyResult instanceof BaseRsp){
            BaseRsp baseRsp = (BaseRsp) bodyResult;
            // 设置
            baseRsp.setCode(999);
        }
        
        return bodyResult;
    }
}

上面我们分别重写了postBeforeHttpMetadata、postSendHttpRequest、postAfterHttpResponseBodyResult三个生命周期的钩子方法去完成我们的需求,在发送请求前对请求体进行加签、在发送请求时动态获取令牌重新构建请求体和打印日志、在发送请求后给响应对象设置code为999。

最后

gitHub代码地址,如果觉得项目有用,可以star下感谢

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1917226.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

策略模式(大话设计模式)C/C++版本

策略模式 商场收银软件 根据客户所购买商品的单价和数量来收费 需求分析&#xff1a; 1. 输入单价数量 > 界面逻辑 2. 计算&#xff08;可能打折或者促销&#xff09; > 业务逻辑 3. 输出结果 > 界面逻辑感觉和计算器的逻辑流程差不多&#xff0c;可以用简单工厂模式…

浪潮天启防火墙TQ2000远程配置方法SSL-xxx、L2xx 配置方法

前言 本次设置只针对配置VXX&#xff0c;其他防火墙配置不涉及。建议把防火墙内外网都调通后再进行Vxx配置。 其他配置可参考&#xff1a;浪潮天启防火墙配置手册 配置SSLVxx 在外网端口开启SSLVxx信息 开启SSLVxx功能 1、勾选 “启用SSL-Vxx” 2、设置登录端口号&#xff0…

Unity3D 太空大战射击游戏

一、前言 本案例是初级案例&#xff0c;意在帮助想使用unity的初级开发者能较快的入门&#xff0c;体验unity开发的方便性和简易性能。 本次我们将使用团结引擎进行开发&#xff0c;帮助想体验团结引擎的入门开发者进行较快的环境熟悉。 本游戏案例以太空作战为背景&#xff0c…

如何分析软件测试中发现的Bug!

假如你是一名软件测试工程师&#xff0c;每天面对的就是那些“刁钻”的Bug&#xff0c;它们像是隐藏在黑暗中的敌人&#xff0c;时不时跳出来给你一个“惊喜”。那么&#xff0c;如何才能有效地分析和处理这些Bug&#xff0c;让你的测试工作变得高效且有趣呢&#xff1f;今天我…

Threadlocal使用获取最后更新人信息

Threadlocal 的作用范围是一个线程&#xff0c;tomcat启动默认开启一个线程 首先点击登录&#xff0c;登录方法会返回token 拿到token后放在请求头中发送商品的插入请求&#xff0c;在插入是设置拿到token中的nickName&#xff08;花名&#xff09;放入&#xff08;lastUpdate…

C 语言中如何实现字符串的拼接?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01; &#x1f4d9;C 语言百万年薪修炼课程 【https://dwz.mosong.cc/cyyjc】通俗易懂&#xff0c;深入浅出&#xff0c;匠心打磨&#xff0c;死磕细节&#xff0c;6年迭代&…

轻松搭建RAG:澳鹏RAG开发工具

我们很高兴地宣布推出RAG开发工具&#xff0c;这是澳鹏大模型智能开发平台的一项新功能。此功能可帮助团队轻松创建高质量的检索增强生成 (RAG) 模型。 什么是 RAG&#xff1f; 检索增强生成 (RAG) 通过利用大量外部数据源&#xff08;例如企业的知识库&#xff09;显著增强了…

git查看版本,查看安装路径、更新版本

git version 查看版本 git update-git-for-windows 更新版本 git version 查看版本

美客多卖家必备:自养号测评补单技术的实战策略

构建美客多&#xff08;MercadoLibre&#xff09;自养号测评体系的稳健策略 一、确立目标与前期筹备 深入理解平台规范&#xff1a;首要任务是深入研究美客多平台的规则与指导方针&#xff0c;确保所有行动均符合平台要求&#xff0c;避免任何违规行为导致账号受限。 明确测评…

光电门验证动量守恒实验

本实验所需器件与第二个实验相同。但是连线方式有所区别&#xff0c;先将Arduino的电源输出接到两个光电门&#xff0c;然后再将光电门1的信号输出线接到Arduino的第10个端口&#xff0c;光电门2的信号输出线接到Arduino的第11个端口。对Arduino写入下列程序&#xff08;只有主…

删除【此电脑】中设备和驱动器下的迅雷下载方法

删除【此电脑】中设备和驱动器下的迅雷下载方法 我们安装迅雷下载、百度网盘、WPS等软件后&#xff0c;在【此电脑】–> 【设备和驱动器】目录下会看到这些驱动器的快捷方式&#xff0c;可以使用删除注册表的方式删除这些东西 启动注册表管理器 首先使用键盘快捷键 Win …

新零售起盘案例「半藏酱酒」布局路径,半藏总院分院招商模式

在当前白酒市场中&#xff0c;一款名为半藏酒的酒品以其独特的新零售模式引起了广泛关注。这种模式不同于传统销售方式&#xff0c;通过多种创新玩法&#xff0c;实现了销售与品牌推广的双重目标&#xff0c;让我们一起来看看细节。 半藏酒的分级代理制度将代理商分为两个层级&…

“未来城市发展之窗”2024上海城博会

随着2024年上海城市博览会的临近&#xff0c;招商工作正火热进行中&#xff0c;且已逐渐接近尾声。这场被誉为“城市未来之窗”的盛会&#xff0c;汇聚了全球各地的城市管理者、建筑师、规划师、投资者以及科技创新者&#xff0c;共同探讨城市发展的未来趋势和解决方案。 一、城…

JavaWeb-js(4)

js事件 在前端页面中&#xff0c;js程序大多数是由事件来驱动的&#xff0c;当触发某些事件的时候&#xff0c;可以使用js负责响应。 js事件由三部分组成: 事件源——》指的是被触发的对象; 事件类型——》如何触发的事件&#xff0c;如:鼠标单击、双击、键盘操作等;…

2024最新最全【Java】全栈,零基础入门到精通

Java基础 本文章是作者的学习笔记&#xff0c;帮助初学者快速入门&#xff0c;内容讲的不是很细&#xff0c;适合初学者&#xff0c;不定时更新。 目录 Java基础数据类型1.基本类型(primitive type)1-1 整数类型1-2 浮点类型1-3 字符类型1-4 boolean类型 2.引用数据类型3.类型…

2024.7.11最新版IDM破解,操作简单

前言 IDM的强劲对手&#xff0c;100%免费&#xff0c;如果破解IDM失败&#xff0c;推荐使用FDM&#xff0c;下载地址&#xff1a;Free Download Manager 破解步骤 打开PowerShell&#xff0c;非CMD 在左下角开始菜单右键点击后选择PowerShell&#xff0c;注意不是打开CMD。…

大模型时代的基础架构:大模型算力中心建设指南

&#x1f482; 个人网站:【 摸鱼游戏】【网址导航】【神级代码资源网站】&#x1f91f; 一站式轻松构建小程序、Web网站、移动应用&#xff1a;&#x1f449;注册地址&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交…

hbase学习

hbase学习 hbase概述&#xff1a; HBase 是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统&#xff0c;用于存储海量的结构化或者半结构化&#xff0c;非结构化的数据&#xff08;底层是字节数组做存储的&#xff09; HBase是Hadoop的生态系统之一&#xff0c;是建立在…

开源浏览器引擎:WebKit简介及工作流程、优缺点

WebKit是一个开源的浏览器引擎&#xff0c;最初由苹果公司基于KHTML&#xff08;K Desktop Environment的HTML渲染引擎&#xff09;开发&#xff0c;并广泛应用于Safari浏览器&#xff0c;后来也被其他多款浏览器和应用采用。WebKit负责解析HTML、CSS、JavaScript等网页内容&am…

【动态规划Ⅳ】二维数组的动态规划——最小路径和

二维数组的动态规划 最小路径和64. 最小路径和原地修改数组定义二维数组进行状态转移优化&#xff1a;用 一维数组进行状态转移相似题目&#xff1a;LCR 166. 珠宝的最高价值 120. 三角形最小路径和原地修改数组定义二维数组进行状态转移一维数组进行状态转移自底向上&#xff…