M功能-open feign的使用-支付系统(四)

news2024/9/20 22:35:34

target:离开柬埔寨倒计时-219day

在这里插入图片描述

这张图片一直是我idea的背景图,分享出来啦…

前言

支付平台使用的是基于springcloud的微服务,服务之间的调用都是使用openfeign,而我们每个服务对外暴露的接口响应都会在外部封装一层code之类的信息(二进制下载除外,如:图片、文件下载等)。所以我们内部服务调用时也会有这一层的封装。

这层封装产生的问题

封装结构类,所有响应统一使用该封装类

package com.littlehow;

import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;

/**
 * @author littlehow
 * @since 5/27/24 18:13
 */
@Slf4j
@Setter
@Getter
@Accessors(chain = true)
public class BaseResp<T> {
    // 成功的code和message
    private static final String SUCCESS_RESP_CODE = "0";
    private static final String SUCCESS_MSG = "success";
    private static final String TRACE_ID = "X-B3-TraceId";

    @ApiModelProperty(value = "状态码", required = true)
    private String code;

    @ApiModelProperty(value = "返回信息")
    private String msg;

    @ApiModelProperty(value = "返回数据", required = true)
    private T data;

    @ApiModelProperty(value = "全局调用id")
    private String traceId;

    @ApiModelProperty(value = "系统毫秒时间戳")
    private Long systemTime;

    private BaseResp() {
        traceId = MDC.get(TRACE_ID);
        systemTime = System.currentTimeMillis();
    }

    /**
     *  成功后响应的数据
     * @param data  - 原始接口的返回值
     * @return - 包装后的数据
     */
    public static <T> BaseResp<T> success(T data) {
        return new BaseResp()
                .setCode(SUCCESS_RESP_CODE)
                .setMsg(SUCCESS_MSG)
                .setData(data);
    }

    /**
     *  失败响应码,国际化也可以基于code来支持
     * @param code     - 错误码
     * @param message  - 错误信息
     * @return
     */
    public static BaseResp fail(String code, String message) {
        return new BaseResp()
                .setCode(code)
                .setMsg(message);
    }

    /**
     *  是否成功,如果失败可以打印日志
     * @return - true表示成功
     */
    public boolean success() {
        boolean isSuccess = SUCCESS_RESP_CODE.equals(code);
        if (!isSuccess) {
            // 这里也可以打印debug日志,使用级别来控制是否打印该行日志
            log.error("errorCode:{}, errorMessage={}", code, msg);
        }
        return isSuccess;
    }
}

统一处理类来进行对响应的统一处理

1.成功响应类

package com.littlehow.advice;

import com.alibaba.fastjson.JSONObject;
import com.littlehow.BaseResp;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


public class SuccessResponseAdvice implements ResponseBodyAdvice<Object> {

    private static Map<Method, Boolean /* ignore advice */> ignore = new ConcurrentHashMap<>();

    /**
     * 这里可以在接口上定义注解以解除统一包装该接口,如果媒体类相关接口
     * @param returnType    -
     * @param converterType -
     * @return -
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        Method method = returnType.getMethod();
        Boolean value = ignore.get(method);
        if (value != null) {
            return value;
        } else {
            boolean result = true;
            Annotation[] annotations = returnType.getMethodAnnotations();
            for (Annotation annotation : annotations) {
                if (annotation instanceof IgnoreAdvice) {
                    result = false;
                    break;
                }
            }
            ignore.put(method, result);
            return result;
        }
    }


    @ResponseBody
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {

        if (body instanceof BaseResp) {
            return body;
        } else if (selectedConverterType == StringHttpMessageConverter.class) {
            // string类型返回对象的话会出现强转异常,因为stringhttpmessageconverter只接收string的参数
            response.getHeaders().add("content-type", MediaType.APPLICATION_JSON_VALUE);
            return JSONObject.toJSONString(BaseResp.success(body));
        }
        return BaseResp.success(body);

    }

}

1.异常响应类

package com.littlehow.advice;

import com.littlehow.BaseResp;
import com.littlehow.error.BizError;
import com.littlehow.error.ErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import java.text.MessageFormat;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;


/**
 * 全局异常处理器基类
 */
@Slf4j
public class BaseGlobalExceptionHandler {

    private static Map<String, String> getErrMessageMap(BindingResult bindingResult) {
        return bindingResult.getAllErrors()
                .stream()
                .collect(Collectors.toMap(error -> {
                            if (error instanceof FieldError) {
                                FieldError fieldError = (FieldError) error;
                                return fieldError.getField();
                            }
                            return error.getObjectName();
                        },
                        item -> Optional.ofNullable(item.getDefaultMessage()).orElse(""),
                        (o1, o2) -> o1));
    }

    /**
     * 拦截自定义的异常
     */
    @ExceptionHandler(BizError.class)
    @ResponseStatus(value = HttpStatus.OK)
    public BaseResp<?> handleBizException(HttpServletRequest request, BizError e) {
        log.error(e.getMessage(), e);
        return BaseResp.fail(e.getCode(), getLocaleMessage(e.getCode(), e.getMessage(),
                request, e.getArgs()));
    }


    /**
     * controller入参校验异常
     */
    @ResponseStatus(value = HttpStatus.OK)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public BaseResp<?> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        log.error("MethodArgumentNotValidException: {}, {}", e.getMessage(), e);

        return BaseResp.fail(ErrorCode.PARAM_ERROR_DESC.getCode(), Objects.toString(getErrMessageMap(e.getBindingResult())));
    }

    /**
     * controller入参校验异常
     */
    @ResponseStatus(value = HttpStatus.OK)
    @ExceptionHandler(value = HttpMessageNotReadableException.class)
    public BaseResp<?> methodArgumentNotValidExceptionHandler(HttpMessageNotReadableException e) {
        log.error("MethodArgumentNotValidException: {}, {}", e.getMessage(), e);

        String errorMessage = e.getLocalizedMessage();
        return BaseResp.fail(ErrorCode.PARAM_ERROR_DESC.getCode(), errorMessage);
    }

    /**
     * controller入参校验异常
     */
    @ResponseStatus(value = HttpStatus.OK)
    @ExceptionHandler(value = ConstraintViolationException.class)
    public BaseResp<?> validationErrorHandler(ConstraintViolationException e) {
        log.error("ConstraintViolationException: {}, {}", e.getMessage(), e);
        return BaseResp.fail(ErrorCode.PARAM_ERROR_DESC.getCode(), e.getMessage());
    }

    /**
     * controller入参校验异常
     */
    @ResponseStatus(value = HttpStatus.OK)
    @ExceptionHandler(value = BindException.class)
    public BaseResp<?> bindExceptionHandler(BindException e) {
        log.error("BindException: {}, {}", e.getMessage(), e);
        return BaseResp.fail(ErrorCode.PARAM_ERROR_DESC.getCode(), e.getMessage());
    }

    @ResponseStatus(value = HttpStatus.OK)
    @ExceptionHandler(value = IllegalArgumentException.class)
    public BaseResp<?> bindArgumentException(IllegalArgumentException e) {
        return BaseResp.fail(ErrorCode.PARAM_ERROR_DESC.getCode(), e.getMessage());
    }


    /**
     * 获取国际化异常信息(支持url中包含lang和浏览器的Accept-Language)
     */
    protected String getLocaleMessage(String code, String defaultMsg, HttpServletRequest request, Object[] params) {
        // 示例就不国际化的处理放出来了
        try {
            return MessageFormat.format(defaultMsg, params);
        } catch (Exception e1) {
            return defaultMsg;
        }
//        try {
//            return messageSource.getMessage(code, params, LocalUtils.getLocale(request));
//        } catch (Exception e) {
//            log.warn("本地化异常消息发生异常: {}, {}", code, params);
//            try {
//                return MessageFormat.format(defaultMsg, params);
//            } catch (Exception e1) {
//                return defaultMsg;
//            }
//        }
    }
}

3.忽略封装的注解

package com.littlehow.advice;

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface IgnoreAdvice {
}

这里封装在服务调用中就比较麻烦了,首先就是接口的定义

在自己的服务中接口的定义一般是下面这样的片段

public interface UserClient {
    /**
	 * 获取用户信息
	 */
	@RequestMapping("/user/get")
    UserInfoResp getUserInfo(QueryUserInfoReq req);
}

public class UserController implements UserClient {
    /**
	* 获取用户信息
	*/
	public UserInfoResp getUserInfo(@RequestBody @Valid QueryUserInfoReq req) {
		// skip detail
		return new UserInfoResp();
	}
}

错误码和异常相关代码

package com.littlehow.error;

/**
 * @author littlehow
 * @since 5/27/24 17:45
 */
public interface IErrCode {
    /**
     * 错误码
     * @return -
     */
    String getCode();

    /**
     * 错误信息
     * @return -
     */
    String getDesc();
}

=======================================================

package com.littlehow.error;

import lombok.Getter;

/**
 * @author littlehow
 * @since 5/27/24 17:47
 */
@Getter
public enum ErrorCode implements IErrCode {
    SYSTEM_BUSY("SS101", "system busy"),
    KEY_INVALID("B1001", "非法密码"),
    PARAM_ERROR_DESC("B1002", "参数错误:{0}"),
    PARAM_ERROR("B1003", "参数错误"),
    PARAM_VERIFY_ERROR("B1004", "验签失败"),
    INVALID_REQUEST("B1005", "非法请求"),
    DATA_ERROR("B1006", "数据错误"),
    DATA_DEAL("B1007", "数据已处理"),
    DATA_EXISTS("B1008", "数据已存在"),
    DATA_NOT_EXISTS("B1009", "数据不存在"),
    CALL_RPC_ERROR("B1011", "调用三方接口失败"),
    ;

    /**
     * 枚举编码
     */
    private String code;

    /**
     * 描述说明
     */
    private String desc;

    ErrorCode(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

==============================================================
package com.littlehow.error;

import lombok.Getter;

/**
 * @author littlehow
 * @since 5/27/24 18:43
 */
@Getter
public class DynamicErrorCode implements IErrCode {
    private final String code;

    private final String desc;

    public DynamicErrorCode(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

======================================================================
package com.littlehow.error;

import org.springframework.util.Assert;

import java.text.MessageFormat;

/**
 * @author littlehow
 * @since 5/27/24 17:49
 */
public class BizError extends RuntimeException {

    protected IErrCode code;

    private String[] args;

    /**
     * @param code 错误码
     */
    public BizError(IErrCode code, String... args) {
        Assert.notNull(code, "code must be not null");
        this.args = args;
        this.code = code;
    }

    public String getMessage() {
        return getFormatMessage();
    }

    public String getCode() {
        return code.getCode();
    }

    public String getFormatMessage() {
        if (args != null && args.length > 0) {
            return MessageFormat.format(code.getDesc(), args);
        }
        return code.getDesc();
    }

    public String[] getArgs() {
        return args;
    }

    public String toString() {
        return "BizError(code=" + code.getCode() + ", desc={}" + getFormatMessage()+")";
    }
}


===========================================================================================
package com.littlehow.error;

import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.util.Collection;

/**
 * @author littlehow
 * @since 5/27/24 18:39
 */
public class BizAssert {
    public static void isTrue(boolean flag, IErrCode message, String... obj) {
        if (!flag) {
            throw new BizError(message, obj);
        }
    }

    public static void notNull(Object obj, IErrCode message, String... argus) {
        if (obj == null) {
            throw new BizError(message, argus);
        }
    }

    public static void isNull(Object obj, IErrCode message, String... argus) {
        if (obj != null) {
            throw new BizError(message, argus);
        }
    }

    public static void isZero(int data, IErrCode message, String... argus) {
        if (data != 0) {
            throw new BizError(message, argus);
        }
    }

    public static void isZero(Long data, IErrCode message, String... argus) {
        if (data != null && data != 0) {
            throw new BizError(message, argus);
        }
    }

    public static void isZero(BigDecimal value, IErrCode message, String... argus) {
        if (value == null || value.compareTo(BigDecimal.ZERO) != 0) {
            throw new BizError(message, argus);
        }
    }

    public static void notEmpty(Collection collection, IErrCode message, String... argus) {
        if (CollectionUtils.isEmpty(collection)) {
            throw new BizError(message, argus);
        }
    }

    public static void isEmpty(Collection collection, IErrCode message, String... argus) {
        if (!CollectionUtils.isEmpty(collection)) {
            throw new BizError(message, argus);
        }
    }

    public static void hasText(String value, IErrCode message, String... argus) {
        if (!StringUtils.hasText(value)) {
            throw new BizError(message, argus);
        }
    }

    public static void hasAmount(BigDecimal value, IErrCode message, String... argus) {
        if (value == null || value.compareTo(BigDecimal.ZERO) <= 0) {
            throw new BizError(message, argus);
        }
    }
}

这里就出现一个巨大的问题,因为feign调用该接口的实际返回是 BaseResp<UserInfoResp>, 处理这个就三种方式

  • 1.单独封装对外的client,全部加上BaseResp
  • 2.内部调用的接口加上特定header头,在处理这些的时候统一按原样返回
  • 3.对feign的调用后结果响应写入单独的解码器

第一个在特别多的服务下明显不够优雅,每次都要去手动包装一下,并且调用方也不一定喜欢外面有一层业务无关的东西

第二个有一点比较烦恼的就是,如果我想要抛出特定的错误码,这个就不能实现,还得用httpcode的错误码形式才行,这样对整个系统现有的http响应模式都有冲击

所以就只能选择第三种方式,写解码器来进行处理

对feign的使用以及改造

为什么会改造feign呢,主要有三个原因

  • 1.feign的统一响应处理如果发现返回值是void或者Void等就不会调用自定义解码器,因为我们自己的接口定义void的时候也会被包装BaseResp,所以此处拿不到信息很痛苦,因为可能有错误码在响应中;
  • 2.feign的响应的解码器出现的异常都会被包装一层DecodeException,这个虽然我可以在统一异常那里处理,但是总感觉不爽,还是喜欢我如果自定义了decode,那么异常就应该是干干净净的我抛出来的;
  • 3.就是重试,feign的重试不方便我动态改变,比如动态关掉某个接口的重试,变成不重试,或者改一些接口的重试次数等,所以这里也改动了。

先看定义的解码器/响应类以及自定义retry

package com.littlehow.feign;

import com.alibaba.fastjson.JSONObject;
import com.littlehow.BaseResp;
import com.littlehow.error.BizAssert;
import com.littlehow.error.BizError;
import com.littlehow.error.DynamicErrorCode;
import com.littlehow.error.ErrorCode;
import com.netflix.loadbalancer.RoundRobinRule;
import com.netflix.loadbalancer.Server;
import feign.FeignException;
import feign.Retryer;
import feign.Util;
import feign.codec.Decoder;
import io.huione.bakong.common.feign.MyFeignRetry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpStatus;

import java.nio.charset.StandardCharsets;
import java.util.Objects;

/**
 * @author littlehow
 * @since 5/27/24 18:49
 */
@Configuration
@Slf4j
@RibbonClients(defaultConfiguration = {FeignRuleConfig.FeignRule.class})
public class FeignRuleConfig {
    @Bean
    public Decoder decoder() throws FeignException {
        return (response, type) -> {
            try {
                HttpStatus httpStatus = HttpStatus.valueOf(response.status());
                if (httpStatus.is2xxSuccessful()) {
                    String feignResp = Util.toString(response.body().asReader(StandardCharsets.UTF_8));
                    FeignBaseResp baseResp = JSONObject.parseObject(feignResp, FeignBaseResp.class);
                    // 错误码统一抛出异常,这里的异常会被feign的decodeException包装,所以那里还需要处理一下
                    BizAssert.isTrue(baseResp.success(), new DynamicErrorCode(baseResp.getCode(), baseResp.getMsg()));
                    ResolvableType returnType = ResolvableType.forType(type);
                    if (returnType.getRawClass().isAssignableFrom(BaseResp.class)) {
                        return JSONObject.parseObject(feignResp, type);
                    } else {
                        return JSONObject.parseObject(baseResp.getData(), type);
                    }
                } else {
                    log.error("resp http status is not 2xx, HttpStatus:{}", httpStatus);
                    throw new BizError(ErrorCode.CALL_RPC_ERROR);
                }
            } catch (BizError e) {
                throw e;
            } catch (Throwable e1) {
                log.error("feign client fail,error message:{}", e1.getMessage(), e1);
                throw new BizError(ErrorCode.SYSTEM_BUSY);
            }
        };
    }

    @Slf4j
    public static class FeignRule extends RoundRobinRule {
        @Override
        public Server choose(Object key) {
            Server server = super.choose(key);
            if (Objects.isNull(server)) {
                log.info("server is null");
                return null;
            }
            log.info("feign rule ---> choose key:{}, final server ip:{}", key, server.getHostPort());
            return server;
        }
    }

    @Bean
    public Retryer feignRetry() {
        return new MyFeignRetry();
    }

}

==============================================================================================

package com.littlehow.feign;

import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;

/**
 * @author littlehow
 * @since 5/27/24 18:13
 */
@Slf4j
@Setter
@Getter
@Accessors(chain = true)
public class FeignBaseResp {
    // 成功的code和message
    private static final String SUCCESS_RESP_CODE = "0";

    @ApiModelProperty("状态码")
    private String code;

    @ApiModelProperty("返回信息")
    private String msg;

    @ApiModelProperty("返回数据,只有这里和BaseResp有区别")
    private String data;

    @ApiModelProperty("全局调用id")
    private String traceId;

    @ApiModelProperty("系统毫秒时间戳")
    private Long systemTime;


    /**
     *  是否成功,如果失败可以打印日志
     * @return - true表示成功
     */
    public boolean success() {
        boolean isSuccess = SUCCESS_RESP_CODE.equals(code);
        if (!isSuccess) {
            // 这里也可以打印debug日志,使用级别来控制是否打印该行日志
            log.error("errorCode:{}, errorMessage={}", code, msg);
        }
        return isSuccess;
    }
}


======================================================================================
package com.littlehow.feign;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import feign.FeignExecuteContext;
import feign.MethodMetadata;
import feign.RetryableException;
import feign.Retryer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @author littlehow
 * @since 5/27/24 18:47
 */
@Slf4j
public class MyFeignRetry extends Retryer.Default {

    /**
     * 不重试
     */
    private static MyFeignRetry NEVER = new MyFeignRetry();
    @Value("${self.feign.retry.enable:false}")
    private boolean openRetry;
    @Value("${self.log.detail:false}")
    private boolean logDetail;
    private Set<String> noRetry;
    private Set<String> canRetry;
    /**
     * 重试时打印重试的请求接口
     */
    private String requestUrl;
    /**
     * 重试时打印调用的类和方法
     */
    private String configKey;

    /**
     * 默认构造方法是不支持重试
     */
    public MyFeignRetry() {
        super(100, TimeUnit.SECONDS.toMillis(1), 1);
    }

    /**
     * 重试两次
     *
     * @param maxPeriod - 最大等待重试时间
     */
    public MyFeignRetry(long maxPeriod, String requestUrl, String configKey, boolean logDetail) {
        super(200, maxPeriod, 3);
        this.requestUrl = requestUrl;
        this.configKey = configKey;
        this.logDetail = logDetail;
    }

    /**
     * 自定义重试参数
     *
     * @param period      - 第一次重试周期
     * @param maxPeriod   - 最大重试周期, 计算过程为super.continueOrPropagate
     * @param maxAttempts - 调用次数
     */
    public MyFeignRetry(long period, long maxPeriod, int maxAttempts) {
        super(period, maxPeriod, maxAttempts);
    }

    @Value("${feign.retry.urls.disable:}")
    public void noRetry(String value) {
        noRetry = splitRetry(value, "disable");
    }

    @Value("${feign.retry.urls.enable:}")
    public void canRetry(String value) {
        canRetry = splitRetry(value, "enable");
    }

    private Set<String> splitRetry(String value, String flag) {
        log.info("change {} retry url config {}", flag, value);
        Set<String> tmp = new HashSet<>();
        if (StringUtils.hasText(value)) {
            String[] v = value.split(",");
            for (String vv : v) {
                vv = vv.trim();
                if (StringUtils.hasText(vv)) {
                    tmp.add(vv);
                }
            }
        }
        return tmp;
    }

    /**
     * 重试继续的判断,不重试则直接抛异常
     *
     * @param e - 可重试
     */
    public void continueOrPropagate(RetryableException e) {
        super.continueOrPropagate(e);
        if (logDetail) {
            log.info("retry-call-feign-info feign-client={}, url={}", configKey, requestUrl);
        }
    }

    @Override
    public Retryer clone() {
        // 判断上下文,选择不同的重试策略
        MethodMetadata methodMetadata = FeignExecuteContext.getMetadata();
        if(Objects.isNull(methodMetadata)) {
            return NEVER;
        }
        String request = methodMetadata.template().url();
        log.info("call-feign-info feign-client={}, url={}", methodMetadata.configKey(), request);
        if (logDetail) {
            log.info("call-feign-detail params={}, timeoutConfig={}", getParams(),
                    JSONObject.toJSONString(FeignExecuteContext.getOptions()));
        }
        // 无论是否开启 retry,都优先使用确认retry的url
        if (canRetry.contains(request)) {
            return new MyFeignRetry(TimeUnit.SECONDS.toMillis(1), request,
                    methodMetadata.configKey(), logDetail);
        }
        if (!openRetry || noRetry.contains(request)) {
            return NEVER;
        }
        // 重试必须每次的初始化对象
        return new MyFeignRetry(TimeUnit.SECONDS.toMillis(1), request,
                methodMetadata.configKey(), logDetail);
    }

    private String getParams() {
        Object[] obj = FeignExecuteContext.getArgus();
        if (obj == null || obj.length == 0) {
            return "no params";
        } else {
            return JSONArray.toJSONString(obj);
        }
    }
}

这里的retry是使用了feign的改造类,这里的feign是基于10.10.1的版本,并且改动的类都在feign包下

首先定义feign的简易上下文

package feign;

public class FeignExecuteContext {
    private static final ThreadLocal<Object[]> ARGUS = new ThreadLocal<>();
    private static final ThreadLocal<MethodMetadata> METADATA = new ThreadLocal<>();
    private static final ThreadLocal<Request.Options> OPTIONS = new ThreadLocal<>();

    static void set(Object[] argus, MethodMetadata metadata, Request.Options options) {
        ARGUS.set(argus);
        METADATA.set(metadata);
        OPTIONS.set(options);
    }

    static void clear() {
        ARGUS.remove();
        METADATA.remove();
        OPTIONS.remove();
    }

    public static Object[] getArgus() {
        return ARGUS.get();
    }

    public static MethodMetadata getMetadata() {
        return METADATA.get();
    }

    public static Request.Options getOptions() {
        return OPTIONS.get();
    }
}

其次是feign结果响应类,主要就是屏蔽void的判定和自定义异常类型的添加和判定方法

/**
 * Copyright 2012-2020 The Feign Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package feign;

import static feign.FeignException.errorReading;
import static feign.Util.ensureClosed;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.concurrent.CompletableFuture;
import feign.Logger.Level;
import feign.codec.DecodeException;
import feign.codec.Decoder;
import feign.codec.ErrorDecoder;

/**
 * The response handler that is used to provide asynchronous support on top of standard response
 * handling
 */
@Experimental
class AsyncResponseHandler {
	private static final long MAX_RESPONSE_BUFFER_SIZE = 8192L;

  private final Level logLevel;
  private final Logger logger;

  private final Decoder decoder;
  private final ErrorDecoder errorDecoder;
  private final boolean decode404;
  private final boolean closeAfterDecode;
  // add littlehow 这里是自定义异常类,如果是这些类,则不需要进行decodeException的包装
  private static final Set<Class<?>> exceptionClass = new HashSet();

  AsyncResponseHandler(Level logLevel, Logger logger, Decoder decoder, ErrorDecoder errorDecoder,
      boolean decode404, boolean closeAfterDecode) {
    super();
    this.logLevel = logLevel;
    this.logger = logger;
    this.decoder = decoder;
    this.errorDecoder = errorDecoder;
    this.decode404 = decode404;
    this.closeAfterDecode = closeAfterDecode;
  }

	// add littlehow 添加自定义异常类
  public static void addExceptionClass(Class<?> e) {
        exceptionClass.add(e);
   }

  boolean isVoidType(Type returnType) {
    return Void.class == returnType || void.class == returnType;
  }

  void handleResponse(CompletableFuture<Object> resultFuture,
                      String configKey,
                      Response response,
                      Type returnType,
                      long elapsedTime) {
    // copied fairly liberally from SynchronousMethodHandler
    boolean shouldClose = true;

    try {
      if (logLevel != Level.NONE) {
        response = logger.logAndRebufferResponse(configKey, logLevel, response,
            elapsedTime);
      }
      if (Response.class == returnType) {
        if (response.body() == null) {
          resultFuture.complete(response);
        } else if (response.body().length() == null
            || response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
          shouldClose = false;
          resultFuture.complete(response);
        } else {
          // Ensure the response body is disconnected
          final byte[] bodyData = Util.toByteArray(response.body().asInputStream());
          resultFuture.complete(response.toBuilder().body(bodyData).build());
        }
      } else if (response.status() >= 200 && response.status() < 300) {
		// add littlehow 这里有调用isVoidType的判定
        // 如果是void的,那么将直接resultFuture.complete(null),所以需要屏蔽掉;
        //if (isVoidType(returnType)) {
        //  resultFuture.complete(null);
        //} else {
          final Object result = decode(response, returnType);
          shouldClose = closeAfterDecode;
          resultFuture.complete(result);
        //}
      } else if (decode404 && response.status() == 404 && !isVoidType(returnType)) {
        final Object result = decode(response, returnType);
        shouldClose = closeAfterDecode;
        resultFuture.complete(result);
      } else {
        resultFuture.completeExceptionally(errorDecoder.decode(configKey, response));
      }
    } catch (final IOException e) {
      if (logLevel != Level.NONE) {
        logger.logIOException(configKey, logLevel, e, elapsedTime);
      }
      resultFuture.completeExceptionally(errorReading(response.request(), response, e));
    } catch (final Exception e) {
      resultFuture.completeExceptionally(e);
    } finally {
      if (shouldClose) {
        ensureClosed(response.body());
      }
    }

  }

  Object decode(Response response, Type type) throws IOException {
        try {
            return this.decoder.decode(response, type);
        } catch (FeignException var4) {
            throw var4;
        } catch (RuntimeException var5) {
            // add littlehow 判断是否拥有这些异常类,是的话就原样抛出
            if (exceptionClass.contains(var5.getClass())) {
                throw var5;
            } else {
                throw new DecodeException(response.status(), var5.getMessage(), response.request(), var5);
            }
        }
    }
}

feign代理的主要处理方法类,主要是添加上下文的设置

/**
 * Copyright 2012-2020 The Feign Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package feign;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import feign.InvocationHandlerFactory.MethodHandler;
import feign.Request.Options;
import feign.codec.Decoder;
import feign.codec.ErrorDecoder;
import static feign.ExceptionPropagationPolicy.UNWRAP;
import static feign.FeignException.errorExecuting;
import static feign.Util.checkNotNull;

final class SynchronousMethodHandler implements MethodHandler {

  private final MethodMetadata metadata;
  private final Target<?> target;
  private final Client client;
  private final Retryer retryer;
  private final List<RequestInterceptor> requestInterceptors;
  private final Logger logger;
  private final Logger.Level logLevel;
  private final RequestTemplate.Factory buildTemplateFromArgs;
  private final Options options;
  private final ExceptionPropagationPolicy propagationPolicy;

  // only one of decoder and asyncResponseHandler will be non-null
  private final Decoder decoder;
  private final AsyncResponseHandler asyncResponseHandler;


  private SynchronousMethodHandler(Target<?> target, Client client, Retryer retryer,
      List<RequestInterceptor> requestInterceptors, Logger logger,
      Logger.Level logLevel, MethodMetadata metadata,
      RequestTemplate.Factory buildTemplateFromArgs, Options options,
      Decoder decoder, ErrorDecoder errorDecoder, boolean decode404,
      boolean closeAfterDecode, ExceptionPropagationPolicy propagationPolicy,
      boolean forceDecoding) {

    this.target = checkNotNull(target, "target");
    this.client = checkNotNull(client, "client for %s", target);
    this.retryer = checkNotNull(retryer, "retryer for %s", target);
    this.requestInterceptors =
        checkNotNull(requestInterceptors, "requestInterceptors for %s", target);
    this.logger = checkNotNull(logger, "logger for %s", target);
    this.logLevel = checkNotNull(logLevel, "logLevel for %s", target);
    this.metadata = checkNotNull(metadata, "metadata for %s", target);
    this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target);
    this.options = checkNotNull(options, "options for %s", target);
    this.propagationPolicy = propagationPolicy;

    if (forceDecoding) {
      // internal only: usual handling will be short-circuited, and all responses will be passed to
      // decoder directly!
      this.decoder = decoder;
      this.asyncResponseHandler = null;
    } else {
      this.decoder = null;
      this.asyncResponseHandler = new AsyncResponseHandler(logLevel, logger, decoder, errorDecoder,
          decode404, closeAfterDecode);
    }
  }

  @Override
  public Object invoke(Object[] argv) throws Throwable {
    try {
      RequestTemplate template = buildTemplateFromArgs.create(argv);
      Options options = findOptions(argv);
      // add littlehow 获取调用的前置信息,这里只是获取一些简单的信息便于下面retryer的调用
      FeignExecuteContext.set(argv, metadata, options);
      Retryer retryer = this.retryer.clone();
      while (true) {
        try {
          return executeAndDecode(template, options);
        } catch (RetryableException e) {
          try {
            retryer.continueOrPropagate(e);
          } catch (RetryableException th) {
            Throwable cause = th.getCause();
            if (propagationPolicy == UNWRAP && cause != null) {
              throw cause;
            } else {
              throw th;
            }
          }
          if (logLevel != Logger.Level.NONE) {
            logger.logRetry(metadata.configKey(), logLevel);
          }
        }
      }
    } finally {
      FeignExecuteContext.clear();
    }
  }

  Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    Request request = targetRequest(template);

    if (logLevel != Logger.Level.NONE) {
      logger.logRequest(metadata.configKey(), logLevel, request);
    }

    Response response;
    long start = System.nanoTime();
    try {
      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;
    }
  }

  long elapsedTime(long start) {
    return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
  }

  Request targetRequest(RequestTemplate template) {
    for (RequestInterceptor interceptor : requestInterceptors) {
      interceptor.apply(template);
    }
    return target.apply(template);
  }

  Options findOptions(Object[] argv) {
    if (argv == null || argv.length == 0) {
      return this.options;
    }
    return Stream.of(argv)
        .filter(Options.class::isInstance)
        .map(Options.class::cast)
        .findFirst()
        .orElse(this.options);
  }

  static class Factory {

    private final Client client;
    private final Retryer retryer;
    private final List<RequestInterceptor> requestInterceptors;
    private final Logger logger;
    private final Logger.Level logLevel;
    private final boolean decode404;
    private final boolean closeAfterDecode;
    private final ExceptionPropagationPolicy propagationPolicy;
    private final boolean forceDecoding;

    Factory(Client client, Retryer retryer, List<RequestInterceptor> requestInterceptors,
        Logger logger, Logger.Level logLevel, boolean decode404, boolean closeAfterDecode,
        ExceptionPropagationPolicy propagationPolicy, boolean forceDecoding) {
      this.client = checkNotNull(client, "client");
      this.retryer = checkNotNull(retryer, "retryer");
      this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors");
      this.logger = checkNotNull(logger, "logger");
      this.logLevel = checkNotNull(logLevel, "logLevel");
      this.decode404 = decode404;
      this.closeAfterDecode = closeAfterDecode;
      this.propagationPolicy = propagationPolicy;
      this.forceDecoding = forceDecoding;
    }

    public MethodHandler create(Target<?> target,
                                MethodMetadata md,
                                RequestTemplate.Factory buildTemplateFromArgs,
                                Options options,
                                Decoder decoder,
                                ErrorDecoder errorDecoder) {
      return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger,
          logLevel, md, buildTemplateFromArgs, options, decoder,
          errorDecoder, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
    }
  }
}

如何让修改的类生效

  • 1.拉取源码重新打包
  • 2.放在项目中
  • 3.编译成字节码,强制加载

我选择的就是强制加载,这里我只贴出加载的形式

package com.littlehow.site;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import com.littlehow.error;

/**
 * @author littlehow
 * @since 2024-04-09 18:44
 */
public class SourceInitial {
    // 优先加载需要的资源类
    public static void initClass() {
        CustomerClassLoader customerClassLoader = new CustomerClassLoader();
        List<String> classNames = new ArrayList<>(ClassSourceManager.classSource.keySet());
        classNames.forEach(className -> {
            try {
                Class clazz = customerClassLoader.loadClass(className);
                if (className.equals("feign.AsyncResponseHandler")) {
                    initException(clazz);
                }
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
        });
    }

    /** 添加异常类  */
    private static void initException(Class clazz) {
        try {
            Method method = clazz.getDeclaredMethod("addExceptionClass", Class.class);
            boolean access = method.isAccessible();
            method.setAccessible(true);
            method.invoke(null, BizError.class);
            method.setAccessible(access);
        } catch (Exception e) {
            // skip
            e.printStackTrace();
        }
    }
}

==============================================================================================
package com.littlehow.site;

import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

/**
 * @author littlehow
 * @since 2024-04-09 17:50
 */
public class ClassSourceManager {
    public static Map<String, byte[]> classSource = new HashMap<>();

    static {
        classSource.put("feign.AsyncResponseHandler", Base64.getDecoder().decode("对应的base64字节码"));
        classSource.put("feign.SynchronousMethodHandler",Base64.getDecoder().decode("对应的base64字节码"));
    }

}

================================================================================================
package com.littlehow.site;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * @author littlehow
 * @since 2024-04-09 17:45
 */
public class CustomerClassLoader extends ClassLoader {
    /**
     * classloader中的保护方法,需要使用反射调用
     */
    private static Method DEFINE_CLASS_METHOD ;

    /**
     * 当前类加载器已经加载过的类
     */
    private static final Map<String, Class<?>> loadedClassMap = new HashMap<>();

    /**
     * 加载主类的类加载器
     */
    private static ClassLoader parent;
    static {
        Method[] methods = ClassLoader.class.getDeclaredMethods();
        for (Method method : methods) {
            if ("defineClass".equals(method.getName()) && method.getParameterCount() == 4) {
                DEFINE_CLASS_METHOD = method;
                method.setAccessible(true);
            }
        }
        parent = CustomerClassLoader.class.getClassLoader();
    }

    @Override
    public Class<?> loadClass(String className) throws ClassNotFoundException {
        synchronized (this) {
            Class<?> c = loadedClassMap.get(className);
            if (c == null) {
                c = findClass(className);
                loadedClassMap.put(className, c);
            }
            return c;
        }
    }

    @Override
    public Class<?> findClass(String className) throws ClassNotFoundException {
        // 加载后就不再保存
        byte[] bts = ClassSourceManager.classSource.remove(className);
        if (bts == null || bts.length == 0) {
            // 如果没有资源类,则调用parent加载器进行加载
            return parent.loadClass(className);
        }
        // 定义给父加载器,保证父类不再去自行加载
        Class<?> clazz = executeDefine(parent, className, bts);
        if (clazz == null) {
            throw new ClassNotFoundException(className);
        }
        return clazz;
    }

    /**
     * 加载类
     * 普通项目的类加载器为应用类加载器:
     * @see sun.misc.Launcher $AppClassLoader
     * 但是当项目被打成springboot的jar包后,加载该类的类加载器是
     * @see org.springframework.boot loader.LaunchedURLClassLoader
     * 该类加载器放置于线程上下文类加载器中
     *
     * @param loader    -  加载器
     * @param className -  类名
     * @param bts       -  字节码
     * @return - 类名对应的class对象
     */
    private Class<?> executeDefine(ClassLoader loader, String className, byte[] bts) {
        try {
            return (Class<?>) DEFINE_CLASS_METHOD.invoke(loader, className, bts, 0, bts.length);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

后记

在这里插入图片描述

feign的调用还是有很多东西没写的,比如灰度调用的实现,上下文灰度在feign里面的实现,其实是在负载均衡器的实现,比如ribbon,或者是feign调用的http连接池的结合公有云组件的一些坑,比如okhttp或者httpclient各自的连接池连接闲置时间默认都在10分钟以上,而aws上的有些组件会对长连接进行管理,闲置250秒就会强制关闭等,导致业务闲置期出现connect reset等错误,反正feign这块的东西特别多,比如自定义错误decode,如404decode,403 、401的http错误码decode等。比如调用前的interceptor切入,比如choose server的逻辑等等,这些只有等有大量时间才能写这个系列,其实之前一段时间还是很想写feign的系列的,但是想要写文的工作量,还要画feign的调用逻辑图等就头大,最终还是放弃了的

今天没有M功能,因为我还是想偶尔出一些源码类的技术文章,虽浅尝辄止,但也算有技术博客的输出,哈哈哈

加油吧littlehow

北京时间:2024-05-27 21:52

金边时间:2024-05-27 20:52

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

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

相关文章

零售EDI:Target DVS EDI项目案例

Target塔吉特是美国一家巨型折扣零售百货集团&#xff0c;与全球供应商建立长远深入的合作关系&#xff0c;目前国内越来越多的零售产品供应商计划入驻Target。完成入驻资格审查之后&#xff0c;Target会向供应商提出EDI对接邀请&#xff0c;企业需要根据指示完成供应商EDI信息…

人脸识别——筛选与删除重复或近似重复数据提高人脸识别的精确度

1. 概述 人脸识别研究通常使用从网上收集的人脸图像数据集&#xff0c;但这些数据集可能包含重复的人脸图像。为了解决这个问题&#xff0c;我们需要一种方法来检测人脸图像数据集中的重复图像&#xff0c;并提高其质量。本文介绍了一种检测人脸图像数据集中重复图像的方法。该…

英语新概念2-回译法-lesson16

第一次回译 if you ___ your car on a wrong place, the traffic police man will find you quickly. If he do not give you the ticket,you are lucky.However,the ___ not all like this,The police man is __ sometimes.I had a holiday in Sweden, I found a ___ in my c…

基于PostGIS的mvt动态矢量切片的后台地图服务和前端调用

目录 一、背景 二、矢量切片 三、Mapbox的矢量切片格式 四、PostGIS生成矢量切片 ST_AsMVT: ST_AsMVTGeom: 五、导入试验数据 六、编写PostGIS函数 七:Java后端实现 八、Openlayers前端调用 一、背景 矢量切片技术目前已成为互联网地图的主流技术,无论是Mapbox还…

项目引用图片后乱码?

用的墨刀原型-标注 把上面图标库的图片下载为png图片在项目中引用了&#xff0c;结果直接项目起来&#xff0c;全都是乱码 经过排查是墨刀图标库图片的问题&#xff0c;把图片重新用管理员账户登录导出图片后再试试

java并发工具类都有哪些

Java中的并发工具类包括&#xff1a; CountDownLatch CountDownLatch允许一个或多个线程等待其他线程完成某些操作。它通常用于线程间的同步&#xff0c;例如在一个线程完成其工作后通知其他线程继续执行。 CyclicBarrier CyclicBarrier是一个同步辅助类&#xff0c;它允许一…

Java项目:基于SSM框架实现的学生就业管理系统分前后台(ssm+B/S架构+源码+数据库+毕业论文+开题报告)

一、项目简介 本项目是一套基于SSM框架实现的学生就业管理系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界面美观、操作简单、功能…

PHP深入理解-PHP架构布局

PHP的架构布局涉及多个层次&#xff0c;让我们一起探讨一下吧&#xff01;&#x1f680; 执行流程&#xff1a;解析为Token&#xff1a;将PHP代码解析成标记&#xff08;tokens&#xff09;。抽象语法树&#xff1a;将语法解析树转换为抽象语法树。Opcodes&#xff1a;将抽象语…

亚马逊测评自养号:轻松掌握运营技巧提升店铺流量

亚马逊平台运营全流程&#xff0c;是每位卖家在电商领域走向成功的必经之路。从产品选择、上架优化&#xff0c;到营销推广、订单处理&#xff0c;每一环节都需精心策划与执行&#xff0c;下面具体介绍亚马逊平台运营全流程是什么&#xff1f; 一、亚马逊平台运营全流程是什么…

云服务器购买之后到部署项目的流程

1.通过账号密码登录百度智能云控制台; 2.进入对应的服务器‘云服务器BBC’ 找到’实例‘即找到对应的服务器列表; 此时通过本地电脑 1.cmd命令提示符 PING 服务器公网地址不通&#xff1b; 2.通过本地电脑进行远程桌面连接不通 原因&#xff1a;没有关联安全组&#xff0c;或者…

如何处理逻辑设计中的时钟域

1.什么是时钟域 2.PLL对时钟域管理 不管是否需要变频变相&#xff0c;在FPGA内部将外部输入时钟从专用时钟引脚扇入后先做PLL处理。如何调用pll&#xff0c;见另一篇文章。 约束输入时钟 creat_clock -period 10 -waveform {0 5} [get_ports {sys_clk}] 3.单bit信号跨时钟…

深入探索python编程中的字典结构

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、字典的特点与基础操作 二、安全访问与哈希函数 三、字典的应用案例 四、总结 在编程的…

MySQL:CRUD初阶(有图有实操)

文章目录 &#x1f4d1;1. 数据库的操作&#x1f324;️1.1 显示当前的数据库&#x1f324;️1.2 创建数据库&#x1f324;️1.3 选中数据库&#x1f324;️1.4 删除数据库 &#x1f4d1;2. 表的操作&#x1f324;️2.1 查看表结构&#x1f324;️2.2 创建表&#x1f324;️2.3…

选项卡式小部件QTabWidget

文章目录 1. 详细介绍2. 常用属性3. 信号4. 常用函数5. 官方示例Tab Dialog QTabWidget提供一堆选项卡式小部件。 1. 详细介绍 选项卡式部件提供一个选项卡栏和一个用于显示与每个选项卡相关的页面的页面区域。 默认情况下&#xff0c;选项卡栏显示在页面区域上方&#xff0c;…

探索编程逻辑中的“卡特牛(continue)”魔法

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言&#xff1a;卡特牛逻辑的魅力 二、卡特牛逻辑的解析 三、卡特牛逻辑的应用实例 …

安全厂商第一站!OASA 走进绿盟科技圆满结束

近日&#xff0c;龙蜥社区安全联盟&#xff08;OASA&#xff09;走进联盟成员单位绿盟科技集团股份有限公司&#xff08;以下简称“绿盟科技”&#xff09;&#xff0c;就未来合作方向&#xff0c;双方进行了一次深入的合作交流。该会议共有 11 位人员出席&#xff0c;有来自绿…

【quarkus系列】创建quarkus第一个应用程序

文章目录 序言环境准备创建项目项目分析程序代码构建访问项目 序言 Quarkus 是一个设计用于 Kubernetes 和云原生环境的 Java 框架&#xff0c;具有快速启动时间、低内存消耗和强大的开发者体验。溪源将带您一步步创建一个简单的 Quarkus 应用程序。 环境准备 在开始之前&am…

C# 使用Aspose生成和修改文档

Aspose库 C#中的Aspose库是一个强大的文件处理库&#xff0c;可以用于各种文件格式的创建、编辑、转换和操作。该库提供了丰富的功能&#xff0c;包括处理文档、电子表格、幻灯片、PDF、图像等多种文件格式&#xff0c;能够轻松实现文件的读取、写入、格式化、样式设置、数据操…

[合集] MySQL 8.x 系列文章清单

↑ 关注“少安事务所”公众号&#xff0c;欢迎⭐收藏&#xff0c;不错过精彩内容~ 从去年7月到现在&#xff0c;自 MySQL 发版模型变更后&#xff0c;已经发布了四个版本&#xff0c;意味着 MySQL 8.x 系列进入了长期支持状态。 当然&#xff0c;目前主要推荐的版本依旧是 MySQ…

vscode中使用conda虚拟环境

每一次配置环境&#xff0c;真的巨烦&#xff0c;网上的资料一堆还得一个个尝试&#xff0c;遂进行整理 1.准备安装好Anaconda 附带一篇测试教程&#xff0c;安装anaconda 2.准备安装vscode 安装地址&#xff1a;Visual Studio Code 3.创建Conda环境 搜索框搜索Anaconda…