搭建大型分布式服务(三十六)SpringBoot 零代码方式整合多个kafka数据源

系列文章目录


文章目录

  • 系列文章目录
  • 前言
      • 一、本文要点
      • 二、开发环境
      • 三、创建项目
      • 四、测试一下
      • 五、小结


前言

让我们来看一下网上是怎样使用SpringBoot整合kafka数据源的,都存在哪些痛点?

痛点一:
手撸kafka配置代码,各种硬编码,无法利用SpringBoot的约定大于配置的优势。
在这里插入图片描述
痛点二:
当项目需要消费的topic,而且他们在不同集群时,需要不断地复制粘贴config和factory,如果项目需要5个不同集群的topic以上,那么这些代码将面临巨大维护压力,并且极其容易出错。
在这里插入图片描述
在这里插入图片描述
痛点三:
假如来了个新业务,也是消费kafka,然后做一些业务逻辑处理,你会发现你不得不又搭建一个新工程,然后重复上述步骤,把代码和配置都复制粘贴一遍。
在这里插入图片描述

分析了上述痛点,我们都知道得把配置和业务代码分离,减轻我们的无意义的重复劳作。那么我们怎么把这些跟kafka相关的东西都抽象成一个模块,让我们精力回归到业务开发本身上呢?

例如简单这样配置就完成SpringBoot和kafka的整合,我们只需要关心com.mmc.multi.kafka.starter.OneProcessorcom.mmc.multi.kafka.starter.TwoProcessor 和业务相关的代码开发。

## topic1的kafka配置
spring.kafka.one.enabled=true
spring.kafka.one.consumer.bootstrapServers=${spring.embedded.kafka.brokers}
spring.kafka.one.topic=mmc-topic-one
spring.kafka.one.group-id=group-consumer-one
spring.kafka.one.processor=com.mmc.multi.kafka.starter.OneProcessor // 业务处理类名称
spring.kafka.one.consumer.auto-offset-reset=latest
spring.kafka.one.consumer.max-poll-records=10
spring.kafka.one.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.one.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer

## topic2的kafka配置
spring.kafka.two.enabled=true
spring.kafka.two.consumer.bootstrapServers=${spring.embedded.kafka.brokers}
spring.kafka.two.topic=mmc-topic-two
spring.kafka.two.group-id=group-consumer-two
spring.kafka.two.processor=com.mmc.multi.kafka.starter.TwoProcessor // 业务处理类名称
spring.kafka.two.consumer.auto-offset-reset=latest
spring.kafka.two.consumer.max-poll-records=10
spring.kafka.two.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.two.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer

国籍惯例,先上源码:Github源码

一、本文要点

本文将介绍通过封装一个starter,来实现多kafka数据源的配置,通过通过源码,可以学习以下特性。系列文章完整目录

  • SpringBoot 整合多个kafka数据源
  • SpringBoot 批量消费kafka消息
  • SpringBoot 优雅地启动或停止消费kafka
  • SpringBoot kafka本地单元测试(免集群)
  • SpringBoot 利用map注入多份配置
  • SpringBoot BeanPostProcessor 后置处理器使用方式
  • SpringBoot 将自定义类注册到IOC容器
  • SpringBoot 注入bean到自定义类成员变量

二、开发环境

  • jdk 1.8
  • maven 3.6.2
  • springboot 2.4.3
  • kafka-client 2.6.6
  • idea 2020

三、创建项目

1、引入SpringBoot的starter和kafka相关jar包。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

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


        <!-- kafka -->
        <!-- https://mvnrepository.com/artifact/org.springframework.kafka/spring-kafka -->
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
        </dependency>

2、在项目工程resources目录下新建META-INF/spring.factories文件,指定配置类,让它完成kafka相关配置。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mmc.multi.kafka.starter.MmcMultiConsumerAutoConfiguration

3、定义一个配置类MmcMultiKafkaProperties,用来接收上述配置。

## topic1的kafka配置
spring.kafka.one.enabled=true
spring.kafka.one.consumer.bootstrapServers=${spring.embedded.kafka.brokers}
spring.kafka.one.topic=mmc-topic-one
spring.kafka.one.group-id=group-consumer-one
spring.kafka.one.processor=com.mmc.multi.kafka.starter.OneProcessor // 业务处理类名称
spring.kafka.one.consumer.auto-offset-reset=latest
spring.kafka.one.consumer.max-poll-records=10
spring.kafka.one.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.one.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer

## topic2的kafka配置
spring.kafka.two.enabled=true
spring.kafka.two.consumer.bootstrapServers=${spring.embedded.kafka.brokers}
spring.kafka.two.topic=mmc-topic-two
spring.kafka.two.group-id=group-consumer-two
spring.kafka.two.processor=com.mmc.multi.kafka.starter.TwoProcessor  // 业务处理类名称
spring.kafka.two.consumer.auto-offset-reset=latest
spring.kafka.two.consumer.max-poll-records=10
spring.kafka.two.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.two.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer

配置类代码如下,其中Consumer是SpringBoot kafka框架源码类,这也是优雅的地方,这样你就可以像使用SpringBoot配置kafka一样使用本starter,使用方式基本保持一致,该支持的配置不用改代码也支持,配置的时候也有智能提示:

@ToString
@Data
@ConfigurationProperties(prefix = "spring")
public class MmcMultiKafkaProperties {

    /**
     * 支持多个kafka配置.
     */
    private Map<String, MmcKafkaProperties> kafka = new HashMap<>();


    /**
     * MmcKafkaProperties.
     */
    @Data
    static class MmcKafkaProperties {
        /**
         * 是否启用.
         */
        private boolean enabled;
        /**
         * 主题(支持配置多个topic,英文逗号分隔).
         */
        private String topic;
        /**
         * 消费组.
         */
        private String groupId;
        /**
         * 并发度.
         */
        private Integer concurrency = 1;
        /**
         * 批量消费.
         */
        private String type = "batch";
        /**
         * 是否在批次内对kafka进行去重,默认为true.
         */
        private boolean duplicate = true;
        /**
         * 处理类.
         */
        private String processor;
        /**
         * 消费者.
         */
        private final KafkaProperties.Consumer consumer = new KafkaProperties.Consumer();
    
        public Map<String, Object> buildConsumerProperties() {
            return new HashMap<>(this.consumer.buildProperties());
        }

    }
}

4、编辑MmcMultiConsumerAutoConfiguration类,根据配置构建不同的kafka factory。

@Slf4j
@Configuration
@EnableConfigurationProperties(MmcMultiKafkaProperties.class)
@ConditionalOnProperty(prefix = "spring.kafka", value = "enabled", matchIfMissing = true)
public class MmcMultiConsumerAutoConfiguration extends BaseConsumerConfiguration {

    @Resource
    private MmcMultiKafkaProperties mmcMultiKafkaProperties;

    @Bean
    public MmcKafkaInputerContainer mmcKafkaInputerContainer(MmcKafkaProcessorFactory factory,
                                                             MmcKafkaBeanPostProcessor beanPostProcessor) throws Exception {

        Map<String, MmcInputer> inputers = new HashMap<>();

        Map<String, MmcMultiKafkaProperties.MmcKafkaProperties> kafkas = mmcMultiKafkaProperties.getKafka();

        // 逐个遍历,并生成consumer
        for (Map.Entry<String, MmcMultiKafkaProperties.MmcKafkaProperties> entry : kafkas.entrySet()) {

            // 唯一消费者名称
            String name = entry.getKey();

            // 消费者配置
            MmcMultiKafkaProperties.MmcKafkaProperties properties = entry.getValue();

            // 是否开启
            if (properties.isEnabled()) {

                // 生成消费者
                MmcKafkaKafkaAbastrctProcessor inputer = factory.buildInputer(name, properties, beanPostProcessor.getSuitableClass());

                // 输入源容器
                ConcurrentMessageListenerContainer<Object, Object> container = concurrentMessageListenerContainer(properties);

                // 设置容器
                inputer.setContainer(container);
                inputer.setName(name);
                inputer.setProperties(properties);

                // 设置消费者
                container.setupMessageListener(inputer);

                // 关闭时候停止消费
                Runtime.getRuntime().addShutdownHook(new Thread(inputer::stop));

                // 直接启动
                container.start();

                // 加入集合
                inputers.put(name, inputer);
            }

        }

        return new MmcKafkaInputerContainer(inputers);
    }

    @Bean
    public MmcKafkaBeanPostProcessor mmcKafkaBeanPostProcessor() {
        return new MmcKafkaBeanPostProcessor();
    }

    @Bean
    public MmcKafkaProcessorFactory processorFactory() {

        return new MmcKafkaProcessorFactory();
    }


}

这里核心点是怎样构建业务处理类MmcKafkaKafkaAbastrctProcessor,为了方便功能的封装,所以这里我们要求所有业务处理类processor都应该继承MmcKafkaKafkaAbastrctProcessor,并定义kafka消息对应的实体类。考虑到很多开发者习惯定义@Service这些类来实现业务逻辑,所以这里我们利用BeanPostProcessor,将所有继承了MmcKafkaKafkaAbastrctProcessor类都收集起来。

public class MmcKafkaBeanPostProcessor implements BeanPostProcessor {

    @Getter
    private Map<String, MmcKafkaKafkaAbastrctProcessor<? extends MmcKafkaMsg>> suitableClass = new ConcurrentHashMap<>();

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        if (bean instanceof MmcKafkaKafkaAbastrctProcessor) {

            MmcKafkaKafkaAbastrctProcessor<? extends MmcKafkaMsg> target = (MmcKafkaKafkaAbastrctProcessor<? extends MmcKafkaMsg>) bean;
            suitableClass.putIfAbsent(beanName, target);
            suitableClass.putIfAbsent(bean.getClass().getName(), target);
        }

        return bean;
    }
}

然后跟配置类指定的processor进行一一绑定。

spring.kafka.one.processor=oneProcessor   // 业务处理类名称
spring.kafka.two.processor=twoProcessor   // 业务处理类名称

@Slf4j
@Service
public class OneProcessor extends MmcKafkaKafkaAbastrctProcessor<DemoMsg> {

    @Resource
    private DemoService demoService;

    @Override
    protected Class<DemoMsg> getEntityClass() {
        return DemoMsg.class;
    }

    @Override
    protected void dealMessage(List<DemoMsg> datas) {

        demoService.dealMessage("one", datas.stream().map(x -> (MmcKafkaMsg) x).collect(Collectors.toList()));

    }


}

同时考虑到很多朋友也喜欢定义一个Class的方式来生成业务处理逻辑,所以这里也做了兼容,于是spring.kafka.xxx.processor 配置,既支持bean的名称,也支持Class的完整路径。

public class MmcKafkaProcessorFactory {

    @Resource
    private DefaultListableBeanFactory defaultListableBeanFactory;

    public MmcKafkaKafkaAbastrctProcessor<? extends MmcKafkaMsg> buildInputer(
            String name, MmcMultiKafkaProperties.MmcKafkaProperties properties,
            Map<String, MmcKafkaKafkaAbastrctProcessor<? extends MmcKafkaMsg>> suitableClass) throws Exception {

        // 如果没有配置process,则直接从注册的Bean里查找
        if (!StringUtils.hasText(properties.getProcessor())) {

            return findProcessorByName(name, properties.getProcessor(), suitableClass);
        }

        // 如果配置了process,则从指定配置中生成实例
        // 判断给定的配置是类,还是bean名称
        if (!isClassName(properties.getProcessor())) {

            throw new IllegalArgumentException("It's not a class, wrong value of ${spring.kafka." + name + ".processor}.");
        }

        // 如果ioc容器已经存在该处理实例,则直接使用,避免既配置了process,又使用了@Service等注解
        MmcKafkaKafkaAbastrctProcessor<? extends MmcKafkaMsg> inc = findProcessorByClass(name, properties.getProcessor(), suitableClass);
        if (null != inc) {
            return inc;
        }

        // 指定的processor处理类必须继承MmcKafkaKafkaAbastrctProcessor
        Class<?> clazz = Class.forName(properties.getProcessor());
        boolean isSubclass = MmcKafkaKafkaAbastrctProcessor.class.isAssignableFrom(clazz);
        if (!isSubclass) {
            throw new IllegalStateException(clazz.getName() + " is not subClass of MmcKafkaKafkaAbastrctProcessor.");
        }

        // 创建实例
        Constructor<?> constructor = clazz.getConstructor();
        MmcKafkaKafkaAbastrctProcessor<? extends MmcKafkaMsg> ins = (MmcKafkaKafkaAbastrctProcessor<? extends MmcKafkaMsg>) constructor.newInstance();

        // 注入依赖的变量
        defaultListableBeanFactory.autowireBean(ins);

        return ins;
    }

    private MmcKafkaKafkaAbastrctProcessor<? extends MmcKafkaMsg> findProcessorByName(String name, String processor, Map<String,
            MmcKafkaKafkaAbastrctProcessor<? extends MmcKafkaMsg>> suitableClass) {

        return suitableClass.entrySet()
                .stream()
                .filter(e -> e.getKey().startsWith(name) || e.getKey().equalsIgnoreCase(processor))
                .map(Map.Entry::getValue)
                .findFirst()
                .orElseThrow(() -> new RuntimeException("Can't found any suitable processor class for the consumer which name is " + name
                        + ", please use the config ${spring.kafka." + name + ".processor} or set name of Bean like @Service(\"" + name + "Processor\") "));
    }


    private MmcKafkaKafkaAbastrctProcessor<? extends MmcKafkaMsg> findProcessorByClass(String name, String processor, Map<String,
            MmcKafkaKafkaAbastrctProcessor<? extends MmcKafkaMsg>> suitableClass) {

        return suitableClass.entrySet()
                .stream()
                .filter(e -> e.getKey().startsWith(name) || e.getKey().equalsIgnoreCase(processor))
                .map(Map.Entry::getValue)
                .findFirst()
                .orElse(null);
    }

    private boolean isClassName(String processor) {

        // 使用正则表达式验证类名格式
        String regex = "^[a-zA-Z_$][a-zA-Z\\d_$]*([.][a-zA-Z_$][a-zA-Z\\d_$]*)*$";
        return Pattern.matches(regex, processor);
    }

}

5、考虑到需要消费的kafka消息五花八门,所以我们才抽象了一个父类MmcKafkaKafkaAbastrctProcessor,要求所有业务逻辑处理类都需要继承它,实则上也是释放了我们劳动力,没必要关注消息的反序列化过程。同时,这里默认使用的都是kafka消息的批量拉取方式,如果你要单条处理,也可以逐条遍历处理。这里使用批量拉取有以下好处:
1、可以提升消息处理速度(下一篇文章将介绍kafka单分区实现10w级消息处理);
2、可以在batch里对消息做聚合和去重;

@Slf4j
@Setter
public abstract class MmcKafkaKafkaAbastrctProcessor<T extends MmcKafkaMsg> implements MmcKafkaStringInputer {

    /**
     * Kafka容器.
     */
    protected ConcurrentMessageListenerContainer<Object, Object> container;
    /**
     * 消费者名称.
     */
    protected String name;
    /**
     * 消费者配置.
     */
    protected MmcMultiKafkaProperties.MmcKafkaProperties properties;

    public MmcKafkaKafkaAbastrctProcessor() {

    }

    public MmcKafkaKafkaAbastrctProcessor(String name, MmcMultiKafkaProperties.MmcKafkaProperties properties) {

        this.name = name;
        this.properties = properties;
    }

    /**
     * 消费kafka消息.
     */
    @Override
    public void onMessage(List<ConsumerRecord<String, String>> records) {

        if (null == records || CollectionUtils.isEmpty(records)) {

            log.warn("{} records is null or records.value is empty.", name);
            return;
        }

        Assert.hasText(name, "You must pass the field `name` to the Constructor or invoke the setName() after the class was created.");
        Assert.notNull(properties, "You must pass the field `properties` to the Constructor or invoke the setProperties() after the class was created.");

        try {

            Stream<T> dataStream = records.stream()
                    .map(ConsumerRecord::value)
                    .flatMap(this::doParse)
                    .filter(Objects::nonNull)
                    .filter(this::isRightRecord);

            if (properties.isDuplicate()) {

                dataStream = dataStream.collect(Collectors.groupingBy(this::buildRoutekey))
                        .entrySet()
                        .stream()
                        .map(this::findLasted)
                        .filter(Objects::nonNull);
            }

            List<T> datas = dataStream.collect(Collectors.toList());
            if (CommonUtil.isNotEmpty(datas)) {

                this.dealMessage(datas);

            }


        } catch (Exception e) {

            log.error(name + "-dealMessage error ", e);
        }
    }


    /**
     * 将kafka消息解析为实体,支持json对象或者json数组.
     *
     * @param json kafka消息
     * @return 实体类
     */
    protected Stream<T> doParse(String json) {

        if (json.startsWith("[")) {

            // 数组
            List<T> datas = JsonUtil.parseJsonArray(json, getEntityClass());
            if (CommonUtil.isEmpty(datas)) {

                log.warn("{} doParse error, json={} is error.", name, json);
                return Stream.empty();
            }

            // 反序列对象后,做一些初始化操作
            datas = datas.stream().peek(this::doAfterParse).collect(Collectors.toList());

            return datas.stream();

        } else {

            // 对象
            T data = JsonUtil.parseJsonObject(json, getEntityClass());
            if (null == data) {

                log.warn("{} doParse error, json={} is error.", name, json);
                return Stream.empty();
            }

            // 反序列对象后,做一些初始化操作
            doAfterParse(data);

            return Stream.of(data);
        }
    }

    /**
     * 反序列对象后,做一些初始化操作.
     *
     * @param data 待处理的实体
     */
    protected void doAfterParse(T data) {

    }

    /**
     * 设置kafka容器.
     *
     * @param container kafka容器
     */
    public void setContainer(ConcurrentMessageListenerContainer<Object, Object> container) {

        this.container = container;
    }

    /**
     * 停止kafka容器.
     */
    public void stop() {
        container.stop();
    }

    /**
     * 启动kafka容器.
     */
    public void start() {
        container.start();
    }

    /**
     * 如果单次拉取的kafka消息有重复,则根据某个字段分组,并取最新的一个.
     *
     * @param entry entry
     * @return 最好更新的实体
     */
    protected  T findLasted(Map.Entry<String, List<T>> entry) {

        try {

            Optional<T> d = entry.getValue().stream()
                    .max(Comparator.comparing(T::getRoutekey));

            if (d.isPresent()) {

                return d.get();
            }

        } catch (Exception e) {

            String content = JsonUtil.toJsonStr(entry.getValue());
            log.error("处理消息出错:{}", e.getMessage() + ": " + content, e);
        }
        return null;
    }

    /**
     * 构造实体类的唯一键.
     *
     * @param t 待处理实体
     * @return 实体类的唯一键
     */
    protected String buildRoutekey(T t) {
        return t.getRoutekey();
    }

    /**
     * 过滤消息.
     *
     * @param t 待处理实体
     * @return true:不过滤,false:过滤
     */
    protected boolean isRightRecord(T t) {
        return true;
    }

    /**
     * 获取反序列的实体类类型.
     *
     * @return 反序列的实体类类型
     */
    protected abstract Class<T> getEntityClass();

    /**
     * 处理消息.
     *
     * @param datas 待处理列表
     */
    protected abstract void dealMessage(List<T> datas) throws ExecutionException, InterruptedException;


}

四、测试一下

1、引入kafka测试需要的jar。参考文章:kafka单元测试

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka-test</artifactId>
            <scope>test</scope>
        </dependency>

2、定义一个消息实体和业务处理类。

@Data
class DemoMsg implements MmcKafkaMsg {

    private String routekey;

    private String name;

    private Long timestamp;

}
@Slf4j
@Service
public class OneProcessor extends MmcKafkaKafkaAbastrctProcessor<DemoMsg> {

    @Resource
    private DemoService demoService;

    @Override
    protected Class<DemoMsg> getEntityClass() {
        return DemoMsg.class;
    }

    @Override
    protected void dealMessage(List<DemoMsg> datas) {

        demoService.dealMessage("one", datas.stream().map(x -> (MmcKafkaMsg) x).collect(Collectors.toList()));

    }


}

3、配置kafka地址和指定业务处理类。

spring.kafka.one.enabled=true
spring.kafka.one.consumer.bootstrapServers=${spring.embedded.kafka.brokers}
spring.kafka.one.topic=mmc-topic-one
spring.kafka.one.group-id=group-consumer-one
spring.kafka.one.processor=com.mmc.multi.kafka.starter.OneProcessor  // 业务处理类名称
spring.kafka.one.consumer.auto-offset-reset=latest
spring.kafka.one.consumer.max-poll-records=10
spring.kafka.one.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.one.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer

4、编写测试类。

@Slf4j
@ActiveProfiles("dev")
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = {MmcMultiConsumerAutoConfiguration.class, DemoService.class, OneProcessor.class})
@TestPropertySource(value = "classpath:application.properties")
@DirtiesContext
@EmbeddedKafka(topics = {"${spring.kafka.one.topic}"})
class AppTest {


    @Resource
    private EmbeddedKafkaBroker embeddedKafkaBroker;

    @Value("${spring.kafka.one.topic}")
    private String topicOne;

    @Value("${spring.kafka.two.topic}")
    private String topicTwo;

    @Test
    void testDealMessage() throws Exception {

        // 模拟生产数据
        produceMessage();

        Thread.sleep(10 * 1000);
    }

    void produceMessage() {

        Map<String, Object> configs = new HashMap<>(KafkaTestUtils.producerProps(embeddedKafkaBroker));
        Producer<String, String> producer = new DefaultKafkaProducerFactory<>(configs, new StringSerializer(), new StringSerializer()).createProducer();

        for (int i = 0; i < 10; i++) {

            DemoMsg msg = new DemoMsg();
            msg.setRoutekey("routekey" + i);
            msg.setName("name" + i);
            msg.setTimestamp(System.currentTimeMillis());

            String json = JsonUtil.toJsonStr(msg);
            producer.send(new ProducerRecord<>(topicOne, "my-aggregate-id", json));
            producer.send(new ProducerRecord<>(topicTwo, "my-aggregate-id", json));
            producer.flush();

        }
    }
}

5、运行一下,测试通过。
在这里插入图片描述

五、小结

将本项目代码构建成starter,就可以大大提升我们开发效率,我们只需要关心业务代码的开发,github项目源码:轻触这里。如果对你有用可以打个星星哦。下一篇,升级本starter,在kafka单分区下实现十万级消费处理速度。

加我加群一起交流学习!更多干货下载、项目源码和大厂内推等着你

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

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

相关文章

一键免费去除视频水印和字幕的AI工具

最近有学员经常让我分享好用的智能抹除视频水印字幕AI工具&#xff0c;今天就给大家分享一个我经常用到的这款工具——腾讯智影&#xff0c;这个平台提供的智能抹除功能&#xff0c;借助这个工具我们可以将视频中不需要的字幕或者水印删除掉。 不过这款工具每天有三次免费次数…

暖阳脚本_ 定制企业软件开发的4个趋势:AI、RPA、云应用、边缘计算

根据 Statista 的统计数据显示&#xff0c;企业级软件市场在全球范围内占据了领先地位&#xff0c;预测到2028年&#xff0c;市场规模将接近3760亿美元。企业应用软件市场的稳健增长&#xff0c;甚至在经济不景气的时候也能持续&#xff0c;这充分表明软件解决方案对于提升企业…

【Linux】第十八站:进程等待

文章目录 一、进程等待的必要性1.进程等待是什么2.进程等待的必要性3.为什么要进程等待呢&#xff1f; 二、进程等待的方法1.问题2.wait3.waitpid4.status的原理5.等待失败6.与status有关的两个宏7.options 一、进程等待的必要性 1.进程等待是什么 通过系统调用wait/waitpid&a…

在listener.ora配置文件中配置listener 1527的监听并且使用tnsnames连接测试

文章目录 前言&#xff1a;一、命令语句实现1、监听介绍2、编辑 listener.ora 文件&#xff1a;寻找配置文件对配置文件进行配置 3、重启监听4、配置TNS 二、图形化界面实现1、listener.ora文件配置2、tnsnames.ora文件配置 三、测试连接 前言&#xff1a; 命令实现和图形化实…

某60区块链安全之51%攻击实战学习记录

区块链安全 文章目录 区块链安全51%攻击实战实验目的实验环境实验工具实验原理攻击过程 51%攻击实战 实验目的 1.理解并掌握区块链基本概念及区块链原理 2.理解区块链分又问题 3.理解掌握区块链51%算力攻击原理与利用 4.找到题目漏洞进行分析并形成利用 实验环境 1.Ubuntu1…

Neo4j安装(Docker中安装Neo4j)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

Android File Transfer(安卓文件传输工具)

Android File Transfer 是一款安卓文件传输工&#xff0c;它允许在Mac操作系统和Android设备之间进行文件传输。 该软件通过USB连接将文件从Mac电脑传输到连接的Android设备&#xff0c;或者反过来从Android设备传输文件到Mac电脑。这包括照片、视频、音乐、文档和其他文件类型…

搞定这套Python爬虫面试题,大厂Offer拿到手软

文章目录 1、简述Python 的特点和优点2、Python 有哪些数据类型&#xff1f;3、列表和元组的区别4、Python 是如何运行的5、Python 运行速度慢的原因6、面对 Python 慢的问题&#xff0c;有什么解决办法7、描述一下全局解释器锁 GIL8、深拷贝 浅拷贝9、is 和 的区别10、文件读…

python 随机数生成

生成随机整数 使用 randint() 函数可以生成指定范围内的随机整数。 import random # 生成1到10之间的随机整数 random_int random.randint(1, 10) print(random_int) 生成随机浮点数 random() 方法用于生成 0 到 1 之间的随机浮点数。 import random # 生成0到1之间…

【性能测试】Jenkins+Ant+Jmeter自动化框架的搭建思路

前言 前面讲了Jmeter在性能测试中的应用及扩展。随着测试的深入&#xff0c;我们发现在性能测试中也会遇到不少的重复工作。 比如某新兴业务处于上升阶段&#xff0c;需要在每个版本中&#xff0c;对某些新增接口进行性能测试&#xff0c;有时还需要在一天中的不同时段分别进行…

Fe-safe 2023 新功能介绍

Fe-safe的功能增强 3DEXPERIENCE中的更新 疲劳耐久性分析界面集成到结构和力学分析App的UX和数据模型当中&#xff0c;支持在同一个App中和同一个仿真对象中定义分析步、相互作用、边界条件、载荷以及耐久性特征。 增强了焊接疲劳功能&#xff0c;在22xGA中集成了Verity的线…

geoserver的ECQL查询

ECQL Reference — GeoServer 2.24.x User Manual CQL and ECQL — GeoServer 2.24.x User Manual ECQL是CQL的扩展&#xff0c;类似sql查询&#xff0c;比ogc的xml格式简单&#xff0c;可以应用在wfs和wms查询上。 通过可视化页面查看过滤效果&#xff0c;默认视图 主键不会…

23111705[含文档+PPT+源码等]计算机毕业设计SSM框架美妆商城全套电商购物

文章目录 **软件开发环境及开发工具&#xff1a;****项目功能介绍&#xff1a;****论文截图&#xff1a;****实现&#xff1a;****代码片段&#xff1a;** 编程技术交流、源码分享、模板分享、网课教程 &#x1f427;裙&#xff1a;776871563 软件开发环境及开发工具&#xff…

C++——map和set

作者&#xff1a;几冬雪来 时间&#xff1a;2023年11月17日 内容&#xff1a;C板块map和set知识讲解 目录 前言&#xff1a; map与set与关联式容器&#xff1a; set底层&#xff1a; set的书写&#xff1a; insert&#xff1a; erase&#xff1a; lower_bound与upper_b…

【139.单词拆分】

目录 一、题目解析二、算法原理三、代码实现 一、题目解析 二、算法原理 三、代码实现 class Solution { public:bool wordBreak(string s, vector<string>& wordDict) {int n s.size();unordered_set<string> hash;for (auto x : wordDict) hash.insert(x);…

目前比较好用的护眼台灯?小白入门最适合的护眼台灯推荐

随着人们对家庭环境艺术的重视&#xff0c;台灯因其摆设在桌案台几上的特殊地位&#xff0c;也要进求特有的装饰效果。家居用台灯开始逐新分流为工艺台灯和书写台灯两类。前者追求外观效果&#xff0c;将发展思路放在材质的创新、造型的求异上&#xff0c;以配合风格多样的家居…

蓝牙耳机仓设计的单芯片解决方案

对于一款优秀的TWS耳机来说&#xff0c;除了耳机本身的音频配置&#xff0c;充电仓也是极为重要的一环。因为与传统有线耳机由设备电池供电不同&#xff0c;缺少了耳机仓&#xff0c;TWS耳机就完全的失去了充电的途径&#xff0c;设备在耗尽电量基本就告别使用了&#xff0c;因…

【GUI】-- 08 JButton、JRadioButton、JCheckBox

GUI编程 03 Swing 3.5 JButton 图片置于按钮之上的JButton&#xff1a; package com.duo.lesson05;import javax.swing.*; import java.awt.*; import java.net.URL;public class JButtonDemo01 extends JFrame {public JButtonDemo01() {Container contentPane getConten…

【Spring】使用三方包进行数据源对象(数据库)管理

在这里使用alibaba的druid来连接数据库&#xff0c;然后再Spring Config下配置数据库 目录 第一步&#xff1a;在pom.xml中导入坐标第二步&#xff1a;在bean中配置连接注 第一步&#xff1a;在pom.xml中导入坐标 在dependencies下写&#xff1a; <dependency><grou…

wpf devexpress显示总结

这个教程示范如何显示总结对于列分组和单个数据行。这个教程基于前一篇 GridControl 可以计算如下总结&#xff1a; 这个数据列&#xff08;Count&#xff09; 这个最大和最小值&#xff08;Max和Min&#xff09;。 总结和平均值&#xff08;Sum和平均值&#xff09; 自定义…