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

前言

如果你项目里还在用传统的编程式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.mfbz.cn/a/797273.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Wamp】局域网设备访问WampServer | 使用域名访问Wamp | Wamp配置HTTPS

局域网设备访问WampServer 参考&#xff1a;https://www.jianshu.com/p/d431a845e5cb 修改Apache的httpd.conf文件 D:\Academic\Wamp\program\bin\apache\apache2.4.54.2\conf\httpd.conf 搜索 Require local 和Require all denied&#xff0c;改为Require all granted <…

【Caffeine】⭐️SpringBoot 项目整合 Caffeine 实现本地缓存

目录 &#x1f378;前言 &#x1f37b;一、Caffeine &#x1f37a;二、项目实践 2.1 环境准备 2.2 项目搭建 2.3 接口测试 ​&#x1f49e;️三、章末 &#x1f378;前言 小伙伴们大家好&#xff0c;缓存是提升系统性能的一个不可或缺的工具&#xff0c;通过缓存可以避免大…

[安洵杯 2019]easy_web1

知识点&#xff1a; 1.base64加解密 2.md5加解密 3.md5碰撞绕过强类型比较 4.Linux命令绕过 进入页面发现url地址中存在 img参数和一个cmd参数&#xff0c;img参数看上去像是base64编码&#xff0c;可以去尝试一下解码. 进行了两次base64解密得到3535352e706e67看着像16进制那么…

log4j2的日志框架(详细,springboot和异步日志的实现)

目录 log4j2的介绍 Log4j2的性能 SpringBoot中的使用Log4j2 log4j2的进阶--异步日志 AsyncAppender方式 AsyncLogger方式 log4j2的介绍 Apache Log4j 2是对Log4j的升级版&#xff0c;参考了logback的一些优秀的设计&#xff0c;并且修复了一些问题&#xff0c;因此带 来…

谷粒商城实战笔记-29~34-前端基础 - ES6

文章目录 零&#xff0c;安装Live Server插件一&#xff0c;创建前端工程1&#xff0c;创建工程2&#xff0c;在工程ES6中创建一个html文件 二&#xff0c;ES6 简介1&#xff0c;ES6 的历史 三&#xff0c;前端基础ES61&#xff0c;let 和 const1.1&#xff0c;let1.1.1 严格的…

蔚来汽车:拥抱TiDB,实现数据库性能与稳定性的飞跃

作者&#xff1a; Billdi表弟 原文来源&#xff1a; https://tidb.net/blog/449c3f5b 演讲嘉宾&#xff1a;吴记 蔚来汽车Tidb爱好者 整理编辑&#xff1a;黄漫绅&#xff08;表妹&#xff09;、李仲舒、吴记 本文来自 TiDB 社区合肥站走进蔚来汽车——来自吴记老师的演讲…

C++ | Leetcode C++题解之第232题用栈实现队列

题目&#xff1a; 题解&#xff1a; class MyQueue { private:stack<int> inStack, outStack;void in2out() {while (!inStack.empty()) {outStack.push(inStack.top());inStack.pop();}}public:MyQueue() {}void push(int x) {inStack.push(x);}int pop() {if (outStac…

QML 鼠标和键盘事件

学习目标&#xff1a;Qml 鼠标和键盘事件 学习内容 1、QML 鼠标事件处理QML 直接提供 MouseArea 来捕获鼠标事件&#xff0c;该操作必须配合Rectangle 获取指定区域内的鼠标事件, 2、QML 键盘事件处理&#xff0c;并且获取对OML直接通过键盘事件 Keys 监控键盘任意按键应的消…

PHP贵州旅游攻略系统-计算机毕业设计源码16663

目 录 第 1 章 引 言 1.1 选题背景与意义 1.2 国内外研究现状 1.3 论文结构安排 第 2 章 系统的需求分析 2.1 系统可行性分析 2.1.1 技术方面可行性分析 2.1.2 经济方面可行性分析 2.1.3 法律方面可行性分析 2.1.4 操作方面可行性分析 2.2 系统功能需求分析 2.3 系…

RK3568笔记三十五:LED驱动开发测试

若该文为原创文章&#xff0c;转载请注明原文出处。 字符设备驱动程序的基本框架&#xff0c;主要是如何申请及释放设备号、添加以及注销设备&#xff0c;初始化、添加与删除 cdev 结构体&#xff0c;并通过 cdev_init 函数建立 cdev 和 file_operations 之间的关联&#xff0c…

IoTDB 集群高效管理:一键启停功能介绍

如何快速启动、停止 IoTDB 集群节点的功能详解&#xff01; 在部署 IoTDB 集群时&#xff0c;对于基础的单机模式&#xff0c;启动过程相对简单&#xff0c;仅需执行 start-standalone 脚本来启动 1 个 ConfigNode 节点和 1 个 DataNode 节点。然而&#xff0c;对于更高级的分布…

9. Python的魔法函数

Python中的魔法函数 在Python中魔法函数是在为类赋能&#xff0c;使得类能够有更多操作。通过重写类中的魔法函数&#xff0c;可以完成很多具体的任务 1. __str__ 通过str魔法函数&#xff0c;可以设置对类的实例的 print() 内容 2. __len__ 通过len魔法函数&#xff0c;可…

在 vite+vue3+electron 中使用 express

文章目录 一、Vite Vue3 Electron 项目的搭建二、搭建 express 环境1、安装 express 框架所需依赖2、创建 express 项目3、配置路由4、启动 express 服务5、启动 electron 并获取数据 三、项目打包 一、Vite Vue3 Electron 项目的搭建 详细的项目构建和打包可参考另一篇文…

【数学建模】——力学模型建立的基本理论及方法

目录 一、基本理论 1. 牛顿力学 1.1 牛顿第一定律&#xff08;惯性定律&#xff09; 1.2 牛顿第二定律&#xff08;动力学定律&#xff09; 1.3 牛顿第三定律&#xff08;作用反作用定律&#xff09; 2. 能量守恒定律 2.1 动能和势能 2.2 能量守恒 3. 动量守恒定律…

【银河麒麟服务器操作系统】系统夯死分析及处理建议

了解银河麒麟操作系统更多全新产品&#xff0c;请点击访问麒麟软件产品专区&#xff1a;https://product.kylinos.cn 服务器环境以及配置 【机型】物理机 处理器&#xff1a; Intel 内存&#xff1a; 512G 整机类型/架构&#xff1a; X86_64 【内核版本】 4.19.90-25…

Perl语言之数组

Perl数组可以存储多个标量&#xff0c;并且标量数据类型可以不同。   数组变量以开头。访问与定义格式如下&#xff1a; #! /usr/bin/perl arr("asdfasd",2,23.56,a); print "输出所有:arr\n"; print "arr[0]$arr[0]\n"; #输出指定下标 print…

时间序列分析(Spss)

时间序列也称动态序列。指标数值按照时间顺序排列而成的数值序列。分三部分&#xff1a;描述过去&#xff0c;分析规律、预测未来。时间序列分析中常用的三种模型&#xff1a;季节分解、指数 平滑方法和ARIMA模型。 时期数据可加&#xff0c;时点数据不可加。&#xff08;灰色预…

北摩高科应邀参加空客供应商大会

民航市场一直以来都是北摩高科重要的战略发展方向&#xff0c;进入国际航空巨头供应链体系也是公司的长期愿景。7月9日至10日&#xff0c;北摩高科公司应邀参与空客集团在天津举办的供应商大会及晚宴。 图1&#xff1a;空客集团采购总监Juergen Westermeier与北摩高科领导 会上…

7-4、5、6 react+ipfs上传文件数据及相关配置(react+区块链实战)

7-4、5、6 reactipfs上传文件数据及相关配置&#xff08;react区块链实战&#xff09; 7-4 reactipfs上传文件7-5 reactipfs 上传数据ipfs跨域配置7-6 reactipfs读取ipfs网络数据 7-4 reactipfs上传文件 引入之前安装的ipfs-api 在电脑后台启动ipfs的服务 ipfs daemon&#…

VSCode remote无法链接

报错信息如下&#xff1a; 远程主机密钥变化导致验证失败 无法连接 解决措施&#xff1a; 删除C:\Users\username.ssh\known_hosts中旧的主机密钥条目&#xff0c;重新连接