从开源项目中学习如何自定义 Spring Boot Starter 小组件

news2024/9/23 3:13:39

前言

今天参考的开源组件Graceful Response——Spring Boot接口优雅响应处理器。

具体用法可以参考github以及官方文档。

image-20231214090633021

基本使用

引入Graceful Response组件

项目中直接引入如下maven依赖,即可使用其相关功能。

        <dependency>
            <groupId>com.feiniaojin</groupId>
            <artifactId>graceful-response</artifactId>
            <version>{latest.version}</version>
        </dependency>

开始使用

配置文件可不配置,直接全部默认配置

graceful-response:
  response-style: 1  # 配置响应格式类型(0 or 1)后续介绍
  print-exception-in-global-advice: true # 在全局异常处理器中打印异常,默认不打印

一切之前需在主启动类添加该注解开启功能:@EnableGracefulResponse。(后续介绍)

Controller直接返回我们查询的对象即可,Graceful Response会帮我们封装响应格式(响应格式可配置

Controller

@RestController
public class HelloController {
    
    @Resource
    private UserMapper userMapper;
    
    @GetMapping("/userList")
    public List<User> userList() {
        return userMapper.selectList(null);
    }
}

调用接口:

image-20231214113454576

默认成功/失败状态码和提示消息也都是可以配置的,是不是感觉非常的神奇(一切皆可配)

image-20231214114109561

其他具体用法见官网。

废话不多说,接下来我们直接步入主题探究下自定义starter组件核心步骤

自定义starter组件核心步骤

首先明确一下,自定义starter组件的三个核心步骤:

  1. 自定义注解
  2. 结合Spring AOP实现注解逻辑
  3. 实现starter组件自动装配以及可配置

接下来看开源组件都是怎么做的吧~

看看源码目录结构(麻雀虽小但五脏俱全):

image-20231214103700335

0. 注解使用

@ExceptionMapper注解为例,介绍一下相关功能及实现原理。

Graceful Response引入@ExceptionMapper注解,通过该注解将异常和错误码关联起来,这样Service方法就不需要再维护Response的响应码了,直接抛出业务异常,由Graceful Response进行异常和响应码的关联。 @ExceptionMapper的用法如下:

自定义业务类异常

/**
 * NotFoundException的定义,使用@ExceptionMapper注解修饰
 * code:代表接口的异常码
 * msg:代表接口的异常提示
 */
@ExceptionMapper(code = "1404", msg = "找不到对象")
public class NotFoundException extends RuntimeException {

}

Service接口定义

public interface QueryService {
    UserInfoView queryOne(Query query);
}

Service接口实现

@Service
public class QueryServiceImpl implements QueryService {
    @Resource
    private UserInfoMapper mapper;

    public UserInfoView queryOne(Query query) {
        UserInfo userInfo = mapper.findOne(query.getId());
        if (Objects.isNull(userInfo)) {
            // 这里直接抛自定义异常
            throw new NotFoundException();
        }
        // ……后续业务操作
    }
}

当Service层的queryOne方法抛出NotFoundException时,Graceful Response会进行异常捕获,并将NotFoundException对应的异常码和异常信息封装到统一的响应对象中,最终接口返回以下JSON(默认响应格式

{
  "status": {
    "code": "1404",
    "msg": "找不到对象"
  },
  "payload": {}
}

使用起来十分方便,接下来我们看下具体实现原理。

1. 自定义注解

首先看下注解定义:

/**
 * 异常映射注解.
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ExceptionMapper {

    /**
     * 异常对应的错误码.
     *
     * @return 异常对应的错误码
     */
    String code() default "ERROR";

    /**
     * 异常信息.
     *
     * @return 异常对应的提示信息
     */
    String msg() default "Poor network quality!";

    /**
     * 异常信息是否支持替换
     * 仅当msgReplaceable==ture,且异常实例的message不为空时才能替换
     */
    boolean msgReplaceable() default false;
}

2. 结合Spring AOP实现注解逻辑(核心)

配置类

配置类GracefulResponseProperties跟可配置项相关,所有配置项都映射为该类

@ConfigurationProperties(prefix = "graceful-response") // 配置前缀
public class GracefulResponseProperties {

    /**
     * 在全局异常处理器中打印异常,默认不打印
     */
    private boolean printExceptionInGlobalAdvice = false;

    /**
     * 默认的Response实现类名称,配置了responseClassFullName,则responseStyle不生效
     */
    private String responseClassFullName;

    /**
     * responseStyle的风格,responseClassFullName为空时才会生效
     * responseStyle==null或者responseStyle==0,Response风格为 DefaultResponseImplStyle0
     * responseStyle=1,Response风格为 DefaultResponseImplStyle1
     */
    private Integer responseStyle;

    /**
     * 默认的成功返回码 默认0
     */
    private String defaultSuccessCode = DefaultConstants.DEFAULT_SUCCESS_CODE;

    /**
     * 默认的成功提示 默认OK
     */
    private String defaultSuccessMsg = DefaultConstants.DEFAULT_SUCCESS_MSG;

    /**
     * 默认的失败码 默认1
     */
    private String defaultErrorCode = DefaultConstants.DEFAULT_ERROR_CODE;

    /**
     * 默认的失败提示 默认error
     */
    private String defaultErrorMsg = DefaultConstants.DEFAULT_ERROR_MSG;

    /**
     * Validate异常码,不提供的话默认DefaultConstants.DEFAULT_ERROR_CODE
     */
    private String defaultValidateErrorCode = DefaultConstants.DEFAULT_ERROR_CODE;

    /**
     * 例外包路径
     */
    private List<String> excludePackages;

    /**
     * 不使用@ExceptionMapper和@ExceptionAliasFor修饰的原生异常
     * 是否使用异常信息Throwable类的detailMessage进行返回
     * originExceptionUsingDetailMessage=false,则msg=defaultErrorMsg
     */
    private Boolean originExceptionUsingDetailMessage = false;

    // getter / setter
}

设置响应状态

ResponseStatusFactory:响应状态工厂(仅定义code和msg),默认实现成功默认响应、失败默认响应以及自定义code、msg响应状态

默认实现类:

// 响应状态
public interface ResponseStatus {

    void setCode(String code);

    String getCode();

    void setMsg(String msg);

    String getMsg();
}

// 默认响应状态
public class DefaultResponseStatus implements ResponseStatus {

    private String code;

    private String msg;

    public DefaultResponseStatus() {
    }

    public DefaultResponseStatus(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    @Override
    public void setCode(String code) {
        this.code = code;
    }

    @Override
    public String getCode() {
        return code;
    }

    @Override
    public void setMsg(String msg) {
        this.msg = msg;
    }

    @Override
    public String getMsg() {
        return msg;
    }
}
// 核心实现类
public class DefaultResponseStatusFactoryImpl implements ResponseStatusFactory {

    @Resource
    private GracefulResponseProperties properties;

    // 默认成功响应转态
    @Override
    public ResponseStatus defaultSuccess() {

        DefaultResponseStatus defaultResponseStatus = new DefaultResponseStatus();
        defaultResponseStatus.setCode(properties.getDefaultSuccessCode());
        defaultResponseStatus.setMsg(properties.getDefaultSuccessMsg());
        return defaultResponseStatus;
    }

    // 默认失败响应转态
    @Override
    public ResponseStatus defaultError() {
        DefaultResponseStatus defaultResponseStatus = new DefaultResponseStatus();
        defaultResponseStatus.setCode(properties.getDefaultErrorCode());
        defaultResponseStatus.setMsg(properties.getDefaultErrorMsg());
        return defaultResponseStatus;
    }

    // 自定code、msg状态
    @Override
    public ResponseStatus newInstance(String code, String msg) {
        return new DefaultResponseStatus(code, msg);
    }
}

设置响应格式

ResponseFactory:根据配置项,设置响应格式

默认实现类:

public class DefaultResponseFactory implements ResponseFactory {

    private final Logger logger = LoggerFactory.getLogger(DefaultResponseFactory.class);

    private static final Integer RESPONSE_STYLE_0 = 0;

    private static final Integer RESPONSE_STYLE_1 = 1;

    @Resource
    private ResponseStatusFactory responseStatusFactory;

    @Resource
    private GracefulResponseProperties properties;

    @Override
    public Response newEmptyInstance() {
        try {
            String responseClassFullName = properties.getResponseClassFullName();

            // 配置了Response的全限定名,即实现Response接口,用配置的进行返回
            if (StringUtils.hasLength(responseClassFullName)) {
                Object newInstance = Class.forName(responseClassFullName).getConstructor().newInstance();
                return (Response) newInstance;
            } else {
                // 没有配Response的全限定名,则创建DefaultResponse
                return generateDefaultResponse();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // 响应格式判断
    private Response generateDefaultResponse() {
        Integer responseStyle = properties.getResponseStyle();
        // 未配置或者配置0
        if (Objects.isNull(responseStyle) || RESPONSE_STYLE_0.equals(responseStyle)) {
            return new DefaultResponseImplStyle0();
        } else if (RESPONSE_STYLE_1.equals(responseStyle)) {
            return new DefaultResponseImplStyle1();
        } else {
            logger.error("不支持的Response style类型,responseStyle={}", responseStyle);
            throw new IllegalArgumentException("不支持的Response style类型");
        }
    }

    @Override
    public Response newInstance(ResponseStatus responseStatus) {
        Response bean = this.newEmptyInstance();
        bean.setStatus(responseStatus);
        return bean;
    }

    @Override
    public Response newSuccessInstance() {
        Response emptyInstance = this.newEmptyInstance();
        emptyInstance.setStatus(responseStatusFactory.defaultSuccess());
        return emptyInstance;
    }

    @Override
    public Response newSuccessInstance(Object payload) {
        Response bean = this.newSuccessInstance();
        bean.setPayload(payload);
        return bean;
    }

    @Override
    public Response newFailInstance() {
        Response bean = this.newEmptyInstance();
        bean.setStatus(responseStatusFactory.defaultError());
        return bean;
    }

}

对应配置文件中两种响应格式:

image-20231214140225763

默认响应格式:

public interface Response {
    
    void setStatus(ResponseStatus statusLine);

    ResponseStatus getStatus();

    void setPayload(Object payload);

    Object getPayload();
}

image-20231214142333843

默认格式实现类:

{
  "status": {
    "code": "",
    "msg": ""
  },
  "payload": {}
}
public class DefaultResponseImplStyle0 implements Response {

    private ResponseStatus status;
    
    private Object payload = Collections.emptyMap();

    public DefaultResponseImplStyle0() {
    }

    public DefaultResponseImplStyle0(Object payload) {
        this.payload = payload;
    }

    @Override
    public void setStatus(ResponseStatus responseStatus) {
        this.status = responseStatus;
    }

    @Override
    public ResponseStatus getStatus() {
        return status;
    }

    @Override
    public void setPayload(Object obj) {
        this.payload = obj;
    }

    @Override
    public Object getPayload() {
        return payload;
    }
}

style设置为1,响应格式:

{

    "code": "1404",
    "msg": "找不到对象"
    "data": {}
}

另一种响应实现类

public class DefaultResponseImplStyle1 implements Response {

    private String code;

    private String msg;

    private Object data = Collections.emptyMap();

    @Override
    public void setStatus(ResponseStatus statusLine) {
        this.code = statusLine.getCode();
        this.msg = statusLine.getMsg();
    }

    @Override
    @JsonIgnore
    public ResponseStatus getStatus() {
        return null;
    }

    @Override
    public void setPayload(Object payload) {
        this.data = payload;
    }

    // 这里直接把 payload 忽略了(因此这种响应格式中没有payload字段)
    @Override
    @JsonIgnore
    public Object getPayload() {
        return null;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

核心处理逻辑类

核心处理逻辑类:全局异常处理

/**
 * 全局异常处理.
 */
@ControllerAdvice
@Order(200)
public class GlobalExceptionAdvice implements ApplicationContextAware {

    private final Logger logger = LoggerFactory.getLogger(GlobalExceptionAdvice.class);

    @Resource
    private ResponseStatusFactory responseStatusFactory;

    @Resource
    private ResponseFactory responseFactory;

    private ExceptionAliasRegister exceptionAliasRegister;

    // 配置类
    @Resource
    private GracefulResponseProperties properties;

    /**
     * 异常处理逻辑. 【统一捕获所有异常】
     *
     * @param throwable 业务逻辑抛出的异常
     * @return 统一返回包装后的结果
     */
    @ExceptionHandler({Throwable.class})
    @ResponseBody 
    public Response exceptionHandler(Throwable throwable) {
        // 配置项是否打印异常信息(默认false)
        if (properties.isPrintExceptionInGlobalAdvice()) {
            logger.error("Graceful Response:GlobalExceptionAdvice捕获到异常,message=[{}]", throwable.getMessage(), throwable);
        }
        ResponseStatus statusLine;
        if (throwable instanceof GracefulResponseException) {
            statusLine = fromGracefulResponseExceptionInstance((GracefulResponseException) throwable);
        } else {
            // 校验异常转自定义异常
            statusLine = fromExceptionInstance(throwable);
        }
        // 设置完code、msg之后直接返回
        return responseFactory.newInstance(statusLine);
    }

    private ResponseStatus fromGracefulResponseExceptionInstance(GracefulResponseException exception) {
        String code = exception.getCode();
        if (code == null) {
            code = properties.getDefaultErrorCode();
        }
        return responseStatusFactory.newInstance(code,
                exception.getMsg());
    }

    private ResponseStatus fromExceptionInstance(Throwable throwable) {

        Class<? extends Throwable> clazz = throwable.getClass();

        // 【直接获取抛出异常上的注解】
        ExceptionMapper exceptionMapper = clazz.getAnnotation(ExceptionMapper.class);

        // 1.有@ExceptionMapper注解,直接设置结果的状态
        if (exceptionMapper != null) {
            boolean msgReplaceable = exceptionMapper.msgReplaceable();
            //异常提示可替换+抛出来的异常有自定义的异常信息
            if (msgReplaceable) {
                String throwableMessage = throwable.getMessage();
                if (throwableMessage != null) {
                    return responseStatusFactory.newInstance(exceptionMapper.code(), throwableMessage);
                }
            }
            return responseStatusFactory.newInstance(exceptionMapper.code(),
                    exceptionMapper.msg());
        }

        // 2.有@ExceptionAliasFor异常别名注解,获取已注册的别名信息
        if (exceptionAliasRegister != null) {
            ExceptionAliasFor exceptionAliasFor = exceptionAliasRegister.getExceptionAliasFor(clazz);
            if (exceptionAliasFor != null) {
                return responseStatusFactory.newInstance(exceptionAliasFor.code(),
                        exceptionAliasFor.msg());
            }
        }
        ResponseStatus defaultError = responseStatusFactory.defaultError();

        // 3. 原生异常 + originExceptionUsingDetailMessage=true
        // 如果有自定义的异常信息,原生异常将直接使用异常信息进行返回,不再返回默认错误提示
        if (properties.getOriginExceptionUsingDetailMessage()) {
            String throwableMessage = throwable.getMessage();
            if (throwableMessage != null) {
                defaultError.setMsg(throwableMessage);
            }
        }
        return defaultError;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.exceptionAliasRegister = applicationContext.getBean(ExceptionAliasRegister.class);
    }
}

非空返回值封装处理类(ResponseBodyAdvice)

ResponseBodyAdvice:泛型T为响应data类型,直接Object即可

@ControllerAdvice
@Order(value = 1000)
public class NotVoidResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    private final Logger logger = LoggerFactory.getLogger(NotVoidResponseBodyAdvice.class);

    @Resource
    private ResponseFactory responseFactory;
    @Resource
    private GracefulResponseProperties properties;

    /**
     * 路径过滤器
     */
    private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();

    /**
     * 只处理不返回void的,并且MappingJackson2HttpMessageConverter支持的类型.
     *
     * @param methodParameter 方法参数
     * @param clazz           处理器
     * @return 是否支持
     */
    @Override
    public boolean supports(MethodParameter methodParameter,
                            Class<? extends HttpMessageConverter<?>> clazz) {
        Method method = methodParameter.getMethod();

        // method为空、返回值为void、非JSON,直接跳过
        if (Objects.isNull(method)
                || method.getReturnType().equals(Void.TYPE)
                || !MappingJackson2HttpMessageConverter.class.isAssignableFrom(clazz)) {
            logger.debug("Graceful Response:method为空、返回值为void、非JSON,跳过");
            return false;
        }

        // 有ExcludeFromGracefulResponse注解修饰的,也跳过
        if (method.isAnnotationPresent(ExcludeFromGracefulResponse.class)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Graceful Response:方法被@ExcludeFromGracefulResponse注解修饰,跳过:methodName={}", method.getName());
            }
            return false;
        }

        // 配置了例外包路径,则该路径下的controller都不再处理
        List<String> excludePackages = properties.getExcludePackages();
        if (!CollectionUtils.isEmpty(excludePackages)) {
            // 获取请求所在类的的包名
            String packageName = method.getDeclaringClass().getPackage().getName();
            if (excludePackages.stream().anyMatch(item -> ANT_PATH_MATCHER.match(item, packageName))) {
                logger.debug("Graceful Response:匹配到excludePackages例外配置,跳过:packageName={},", packageName);
                return false;
            }
        }
        logger.debug("Graceful Response:非空返回值,需要进行封装");
        return true;
    }

    // 满足上述条件,封装返回对象(只要是非Void返回值,不做任何配置都会封装返回结果)
    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter methodParameter,
                                  MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> clazz,
                                  ServerHttpRequest serverHttpRequest,
                                  ServerHttpResponse serverHttpResponse) {
        if (body == null) {
            return responseFactory.newSuccessInstance();
        } else if (body instanceof Response) {
            return body;
        } else {
            // 设置data数据
            return responseFactory.newSuccessInstance(body);
        }
    }

}

3. 实现starter组件自动装配以及可配置

全局自动装配类

该类可以将我们自定义的所有类全部加载到Spring容器中。

/**
 * 全局返回值处理的自动配置.
 */
@Configuration
// 将外部配置文件中的属性注入到配置类中(将配置类加载到Spring容器中)
@EnableConfigurationProperties(GracefulResponseProperties.class)
public class AutoConfig {

    @Bean
    // 这个注解很是重要
    @ConditionalOnMissingBean(value = GlobalExceptionAdvice.class)
    public GlobalExceptionAdvice globalExceptionAdvice() {
        return new GlobalExceptionAdvice();
    }

    @Bean
    @ConditionalOnMissingBean(value = ValidationExceptionAdvice.class)
    public ValidationExceptionAdvice validationExceptionAdvice() {
        return new ValidationExceptionAdvice();
    }

    @Bean
    @ConditionalOnMissingBean(NotVoidResponseBodyAdvice.class)
    public NotVoidResponseBodyAdvice notVoidResponseBodyAdvice() {
        return new NotVoidResponseBodyAdvice();
    }

    @Bean
    @ConditionalOnMissingBean(VoidResponseBodyAdvice.class)
    public VoidResponseBodyAdvice voidResponseBodyAdvice() {
        return new VoidResponseBodyAdvice();
    }

    @Bean
    @ConditionalOnMissingBean(value = {ResponseFactory.class})
    public ResponseFactory responseBeanFactory() {
        return new DefaultResponseFactory();
    }

    @Bean
    @ConditionalOnMissingBean(value = {ResponseStatusFactory.class})
    public ResponseStatusFactory responseStatusFactory() {
        return new DefaultResponseStatusFactoryImpl();
    }

    @Bean
    public ExceptionAliasRegister exceptionAliasRegister() {
        return new ExceptionAliasRegister();
    }

    @Bean
    public Init init(){
        return new Init();
    }
}

注解启动全局结果处理入口

通过元注解 @Import(AutoConfig.class) 实际上将 AutoConfig 这个配置类引入到标注了 @EnableGracefulResponse 注解的类中。

引入该组件,只有在某个类上添加了 @EnableGracefulResponse 注解时,AutoConfig 中定义的相关 Bean 才会被注册到 Spring 容器中。可以方便地启用特定功能或配置。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfig.class)
public @interface EnableGracefulResponse {
}

SpringBoot主启动类,添加该注解开启功能:

image-20231214153605858

另一种方式META-INF/spring.factories

如果想引入该组件就直接开启所有功能,可以不用上述全局启动注解

直接在组件项目的 resources 中创建 META-INF 目录,并在此目录下创建一个 spring.factories 文件,将starter组件的自动配置类的类路径写在文件上即可。

resources
	META-INF
		spring.factories
// 直接将自动装配类全限定名放入该文件即可
com.feiniaojin.gracefulresponse.AutoConfig

Spring Boot项目启动时,会扫描外部引入的Jar中的META-INF/spring.factories文件,将文件中配置的类信息装配到Spring容器中)【用的是Java的SPI机制,还没研究后续再说吧】

这样只要引入该组件,组件功能也就集成进来了。【这种不够灵活,视情况用哪种方式】(建议:能用全局启动注解就用全局启动注解)

配置智能提示

组件中记得添加该依赖:

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <version>${spring.boot.version}</version>
            <optional>true</optional>
        </dependency>

组件中添加上述依赖可以用于处理配置类(@ConfigurationProperties 注解标注的类)以及生成相关的元数据。

IDE 支持: 提供更好的集成开发环境(IDE)支持。通过生成的配置元数据,IDE 可以在配置文件中提供智能提示,我们可以更方便地编辑配置。

引入该组件,在修改配置类可以有智能提示

如下:

image-20231214114109561

该文章只介绍了该组件部分功能,有兴趣可以自行研究呀~

最后看一下组件的目录结构(学习一下):

image-20231214153359444

小结

自定义starter组件的三个核心步骤:

  1. 自定义注解
  2. 注解结合AOP实现逻辑
  3. 自动装配和可配置(配置类、自动装配类以及全局启动注解/spring.factories文件

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

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

相关文章

加速数据采集:用OkHttp和Kotlin构建Amazon图片爬虫

引言 曾想过轻松获取亚马逊上的商品图片用于项目或研究吗&#xff1f;是否曾面对网络速度慢或被网站反爬虫机制拦截而无法完成数据采集任务&#xff1f;如果是&#xff0c;那么本文将为您介绍如何用OkHttp和Kotlin构建一个高效的Amazon图片爬虫解决方案。 背景介绍 亚马逊&a…

ES6学习(三):Set和Map容器的使用

Set容器 set的结构类似于数组,但是成员是唯一且不会重复的。 创建的时候需要使用new Set([])的方法 创建Set格式数据 let set1 new Set([])console.log(set1, set1)let set2 new Set([1, 2, 3, 4, 5])console.log(set2, set2) 对比看看Set中唯一 let set3 new Set([1, 1,…

Unity中Shader URP最简Shader框架(整理总结篇)

文章目录 前言一、精简 ShaderGraph 所有冗余代码后的最简 URP Shader二、我们来对比一下 URP Shader 与 BuildInRP Shader 的对应关系 与 区别1、"RenderPipeline""UniversalPipeline"2、面片剔除、深度测试、深度写入、颜色混合 和 BRP 下一致3、必须引入…

Java中的链表

文章目录 前言一、链表的概念及结构二、单向不带头非循坏链表的实现2.1打印链表2.2求链表的长度2.3头插法2.4尾插法2.5任意位置插入2.6查找是否包含某个元素的节点2.7删除第一次出现这个元素的节点2.8删除包含这个元素的所以节点2.9清空链表单向链表的测试 三、双向不带头非循坏…

使用动画曲线编辑器打造炫酷的3D可视化ACE

前言 在制作3D可视化看板时&#xff0c;除了精细的模型结构外&#xff0c;炫酷的动画效果也是必不可少的。无论是复杂的还是简单的动画效果&#xff0c;要实现100%的自然平滑都是具有挑战性的工作。这涉及到物理引擎的计算和对动画效果的数学建模分析。一般来说&#xff0c;只…

前端页面显示的时间格式为:2022-03-18T01:46:08.000+00:00 如何转换为:年-月-日,并根据当前时间判断为几天前

由于后端每条博文的发表时间是以“xxxx—xx—xxxx:xx:xx”的形式显示的&#xff0c; 现在要在前端改成“xxxx年xx月xx日”的形式。 并对10分钟内发表的显示“刚刚”&#xff0c;对24小时内发表的显示“小时前”。 超过24小时&#xff0c;小于48小时&#xff0c;显示“1天前”。…

PFA容量瓶应用工业制造领域PFA定容瓶精确测量的重要性

容量瓶是保证科学、医学和工业等各个领域精确测量的重要工具。这些专门的容量瓶被设计用来在特定的温度下保持精确的液体体积&#xff0c;使它们成为在工作中需要高精确度的专业人士不可或缺的工具。在这份容量瓶终极指南中&#xff0c;今天我们来探讨下这些仪器的重要性&#…

ios苹果app应用程序录屏开发有哪些难点和注意点?

Hello&#xff0c;各位同学们好&#xff0c;我是咕噜铁蛋&#xff0c;老朋友们应该知道我经常关注并分享各种移动应用开发的技术和经验。在这篇文章中&#xff0c;铁蛋将为大家介绍分享苹果iOS录屏开发的难点和注意点&#xff01; 首先&#xff0c;让我们简单了解一下iOS录屏的…

如何在 Eolink Apikit 中发起 TCP/UDP 文档测试

TCP/UDP 是两种常用的网络传输协议。TCP 协议提供可靠的连接&#xff0c;而 UDP 协议提供不可靠的连接。 TCP 协议是面向连接的协议&#xff0c;在建立连接之前&#xff0c;客户端和服务器需要先握手。握手完成后&#xff0c;客户端和服务器之间就会建立一个可靠的连接。在连接…

记录今日将C语言的Windows程序更改为python语言Windows程序,实现子窗口控制,类似微信程序框架最简单的原型

基本思路 为什么要选择python制作Windows应用程序&#xff0c;主要就是源代码直接展示&#xff0c;发现问题随时修改&#xff0c;同时可以不断增加新的功能方便。 由于C语言的Windows程序中结构类型在python中不能使用&#xff0c; 因此我们按照ctypes模块指导意见继承structu…

微服务技术 RabbitMQ SpringAMQP P61-P76

B站学习视频https://www.bilibili.com/video/BV1LQ4y127n4?p61&vd_source8665d6da33d4e2277ca40f03210fe53a 文档资料: 链接&#xff1a;https://pan.baidu.com/s/1P_Ag1BYiPaF52EI19A0YRw?pwdd03r 提取码&#xff1a;d03r 一 初始MQ 1. 同步通讯 2. 异步通讯 3. MQ常…

低代码与自动化:加速软件开发的新趋势

低代码与自动化技术正在逐渐改变软件开发的面貌。随着科技的不断发展&#xff0c;传统的编程方式已经不再是唯一的选择。低代码和自动化技术正在为开发者提供更高效、更灵活的开发环境&#xff0c;使得软件开发变得更加简单、快速和高效。 低代码和自动化技术正在逐渐改变软件开…

理解JSX:提高前端开发效率的关键(上)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

百度搜索展现服务重构:进步与优化

作者 | 瞭东 导读 本文将简单介绍搜索展现服务发展过程&#xff0c;以及当前其面临的三大挑战&#xff1a;研发难度高、架构能力欠缺、可复用性低&#xff0c;最后提出核心解决思路和具体落地方案&#xff0c;期望大家能有所收货和借鉴。 全文4736字&#xff0c;预计阅读时间12…

高级C#技术(二)

前言 本章为高级C#技术的第二节也是最后一节。前一节在下面这个链接 高级C#技术https://blog.csdn.net/qq_71897293/article/details/134930989?spm1001.2014.3001.5501 匿名类型 匿名类型如其名&#xff0c;匿名的没有指定变量的具体类型。 举个例子&#xff1a; 1 创建…

MySQL数据库,视图、存储过程与存储函数

数据库对象&#xff1a; 常见的数据库对象&#xff1a; 视图&#xff1a; 视图是一种虚拟表&#xff0c;本身是不具有数据的占用很少的内存空间。 视图建立在已有表的基础上&#xff0c;视图赖以建立的这些表称为基表。 视图的创建和删除只影响视图本身&#xff0c;不影响对…

案例058:基于微信小程序的智能社区服务管理系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

计算机设计大赛信息可视化设计的获奖经验剖析解读—基于本专栏文章助力4C大赛【全网最全万字攻略-获奖必读】

文章目录 一.中国大学生计算机设计大赛1.1赛道解读1.2 信息可视化设计小类介绍1.2 小类区别解读 二.信息可视化设计赛道获奖经验2.1 四小类作品预览2.1.1 数据可视化小类-优秀参赛作品展览2.1.2 信息图形设计小类-优秀参赛作品展览2.1.3 动态信息影像&#xff08;MG动画&#x…

2024免费mac苹果电脑系统电脑管家CleanMyMac X

macOS已经成为最受欢迎的桌面操作系统之一&#xff0c;它提供了直观、简洁的用户界面&#xff0c;使用户可以轻松使用和管理系统。macOS拥有丰富的应用程序生态系统&#xff1b;还可以与其他苹果产品和服务紧密协作&#xff0c;如iPhone、iPad&#xff0c;用户可以通过iCloud同…

理解JSX:提高前端开发效率的关键(下)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…