Spring Boot自动装配原理以及实践

news2024/11/26 5:50:20

了解自动装配两个核心

@Import注解的作用

@Import说Spring框架经常会看到的注解,它有以下几个作用:

  1. 导入@Configuration类下所有的@bean方法中创建的bean
  2. 导入import指定的bean,例如@Import(AService.class),就会生成AServicebean,并将其导入到Spring容器中。
  3. 结合ImportSelector接口类导如指定类。(后文会展开介绍)

ImportSelector详解

ImportSelector接口则是前者的辅助者,如果我们希望可以选择性的导入一些类,我们就可以继承ImportSelector接口编写一个ImportSelector类,告知容器需要导入的类。就以Spring Boot为例,它有个@EnableAutoConfiguration注解,其工作原理就是基于内部的@Import({AutoConfigurationImportSelector.class})注解将AutoConfigurationImportSelector导入容器中,Spring就会调用其selectImports方法获取需要导入的类,并将这些类导入容器中。

@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
		//返回需要导入的类的字符串数组
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}

使用示例

可能上文的原理对没有接触源码的读者比较模糊,所以我们不妨写一个demo来了解一下这个注解。我们现在有一个需求,希望通过import注解按需将Student类或者User类导入容器中。首先我们看看user类代码,没有任何实现,代码示例如下:

public class User {
}

Student 类代码同理,没有任何实现仅仅做测试使用

public class Student {
}

完成测试类的创建之后,我们就以用户类为例,创建UserConfig 代码如下:

@Configuration
public class UserConfig {

    @Bean
    public User getUser() {
        return new User();
    }
}

然后编写ImportSelector 首先类,编写自己的导入逻辑,可以看到笔者简单实现了一个selectImports方法返回UserConfig的类路径。

public class CustomImportSelector implements ImportSelector {

     private static Logger logger = LoggerFactory.getLogger(CustomImportSelector.class);

    /**
     * importingClassMetadata:被修饰的类注解信息
     */
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {



        logger.info("获取到的注解类型:{}",importingClassMetadata.getAnnotationTypes().toArray());

        // 如果被CustomImportSelector导入的组件是类,那么我们就实例化UserConfig
        if (!importingClassMetadata.isInterface()) {
            return new String[] { "com.example.UserConfig" };
        }

        // 此处不要返回null
        return new String[] { "com.example.StudentConfig" };
    }
}

完成这些步骤我们就要来到最关键的一步了,在Spring Boot启动类中使用@Import导入CustomImportSelector

@SpringBootApplication
@Configuration
@Import(CustomImportSelector.class)
public class DemoApplication {

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

}

为了测试我们编写这样一个controller看看bean是否会导入到容器中

@RestController
public class MyController {

    private static Logger logger = LoggerFactory.getLogger(MyController.class);

    @Autowired
    private User user;

    @RequestMapping("hello")
    public String hello() {
        logger.info("user:{}", user);
        return "hello";
    }
}

结果测试我们发现user不为空,说明CustomImportSelector确实将UserConfig导入到容器中,并将User导入到容器中了。

从源码角度了解ImportSelector工作原理

关于源码分析其实也很好做,感兴趣的读者可以直接在CustomImportSelector打个断点就能知道工作原理了:

在这里插入图片描述

断点之后我们不妨用以终为始的方式了解一下过程,首先入口是AbstractApplicationContextrefresh()方法,它会调用一个invokeBeanFactoryPostProcessors(beanFactory);进行bean工厂后置操作

@Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
		.........
				invokeBeanFactoryPostProcessors(beanFactory);

			........

}
}

步入代码,可以看到容器会不断遍历各个postProcessor 即容器后置处理器,然后执行他们的逻辑

for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
			.....
			//执行各个postProcessor 的逻辑
			invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
}

重点来了,遍历过程中得到一个ConfigurationClassPostProcessor,这个类就会得到我们的CustomImportSelector,然后执行selectImports获取需要导入的类信息,最终会生成一个Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());

如下图所示可以看到configClasses就包含UserConfig

在这里插入图片描述

总结一下核心流程的时序图

在这里插入图片描述

完成上述步骤后ConfigurationClassPostProcessor就会通过这个set集合执行loadBeanDefinitions方法将需要的bean导入到容器中,进行后续IOC操作。

在这里插入图片描述

上图代码如下所示:

//configClasses 中就包含了UserConfig类
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
			configClasses.removeAll(alreadyParsed);

//执行	loadBeanDefinitions	
			this.reader.loadBeanDefinitions(configClasses);

Spring Boot自动装配原理(重点)

了解了import原理后,我们了解Spring Boot自动装配原理也很简单了,我们不妨看看Spring Boot@SpringBootApplication这个注解中包含一个@EnableAutoConfiguration注解,我们不妨点入看看,可以看到它包含一个@Import(AutoConfigurationImportSelector.class)注解,从名字上我们可以知晓这是一个ImportSelector的实现类。

所以我们不妨看看它的selectImports逻辑,可以看到它会通过getAutoConfigurationEntry方法获取需要装配的类,然后通过StringUtils.toStringArray切割返回。所以我们不妨看看getAutoConfigurationEntry

@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}

查看getAutoConfigurationEntry方法,我们可以看到它通过getCandidateConfigurations获取各个xxxxAutoConfigure,并返回结果

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
		//获取所有xxxxAutoConfigure
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
		//移除不需要的
		configurations = removeDuplicates(configurations);
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		checkExcludedClasses(configurations, exclusions);
		configurations.removeAll(exclusions);
		configurations = getConfigurationClassFilter().filter(configurations);
		fireAutoConfigurationImportEvents(configurations, exclusions);
		//返回结果
		return new AutoConfigurationEntry(configurations, exclusions);
	}

在这里插入图片描述

getCandidateConfigurations实际上是会通过一个loadSpringFactories方法,如下所示遍历获取所有含有META-INF/spring.factoriesjar

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
        Map<String, List<String>> result = (Map)cache.get(classLoader);
        if (result != null) {
            return result;
        } else {
            HashMap result = new HashMap();

            try {
            //解析这个配置文件获取所有配置类然后返回
                Enumeration urls = classLoader.getResources("META-INF/spring.factories");

              .....
                return result;
            } catch (IOException var14) {
                throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
            }
        }
    }

最终结果过滤解析,回到我们上文说的beanDefinitionMap中,最终通过IOC完成自动装配。

实践1-手写Spring Boot Starter中间件

了解自动装配我们不妨自己写一个中间件实践一下,现在需求如下,我们希望某些类的接口只有某几个用户可以访问,所以我们希望编写一个中间件判断请求用户是什么身份,如果没有权限则直接返回报错。

首先我们编写一个注解DoDoor ,用key记录传入的用户idreturnJson返回没有权限的响应结果

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoDoor {

    String key() default "";

    String returnJson() default "";

}

然后在编写StarterServiceProperties ,使用ConfigurationPropertiesitstack.door前缀的值和当前类userStr绑定。

/**
 * 通过"itstack.door前缀的配置获取userStr信息
 */
@ConfigurationProperties("itstack.door")
public class StarterServiceProperties {

    private String userStr;

    public String getUserStr() {
        return userStr;
    }

    public void setUserStr(String userStr) {
        this.userStr = userStr;
    }

}

完成后在编写StarterService 这个类会将userStr切割成数组,例如我们传111,222,最终就会得到[111,222]

public class StarterService {

    private String userStr;

    public StarterService(String userStr) {
        this.userStr = userStr;
    }

    public String[] split(String separatorChar) {
        return StringUtils.split(this.userStr, separatorChar);
    }

}

这些佐料写完之后,我们就可以编写一个AOP类了,可以看到这个AOP做的是很简单,就是拦截带有DoDoor的请求,将注解key配置的值和我们的userStr数组比对,若包含则放行,反之拦截。

@Aspect
@Component
public class DoJoinPoint {

    private Logger logger = LoggerFactory.getLogger(DoJoinPoint.class);

    @Autowired
    private StarterService starterService;

    @Pointcut("@annotation(org.itstack.door.annotation.DoDoor)")
    public void aopPoint() {
    }

    @Around("aopPoint()")
    public Object doRouter(ProceedingJoinPoint jp) throws Throwable {
        //获取内容
        Method method = getMethod(jp);
        DoDoor door = method.getAnnotation(DoDoor.class);
        //获取字段值
        String keyValue = getFiledValue(door.key(), jp.getArgs());
        logger.info("itstack door handler method:{} value:{}", method.getName(), keyValue);
        if (null == keyValue || "".equals(keyValue)) return jp.proceed();
        //配置内容
        String[] split = starterService.split(",");
        //白名单过滤
        for (String str : split) {
            if (keyValue.equals(str)) {
                return jp.proceed();
            }
        }
        //拦截
        return returnObject(door, method);
    }

    private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
        Signature sig = jp.getSignature();
        MethodSignature methodSignature = (MethodSignature) sig;
        return getClass(jp).getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
    }

    private Class<? extends Object> getClass(JoinPoint jp) throws NoSuchMethodException {
        return jp.getTarget().getClass();
    }

    //返回对象
    private Object returnObject(DoDoor doGate, Method method) throws IllegalAccessException, InstantiationException {
        Class<?> returnType = method.getReturnType();
        String returnJson = doGate.returnJson();
        if ("".equals(returnJson)) {
            return returnType.newInstance();
        }
        return JSON.parseObject(returnJson, returnType);
    }

    //获取属性值
    private String getFiledValue(String filed, Object[] args) {
        String filedValue = null;
        for (Object arg : args) {
            try {
                if (null == filedValue || "".equals(filedValue)) {
                    filedValue = BeanUtils.getProperty(arg, filed);
                } else {
                    break;
                }
            } catch (Exception e) {
                if (args.length == 1) {
                    return args[0].toString();
                }
            }
        }
        return filedValue;
    }

}

编写我们的AutoConfigure ,根据条件决定上述的类是否导入

@Configuration
@ConditionalOnClass(StarterService.class)
@EnableConfigurationProperties(StarterServiceProperties.class)
public class StarterAutoConfigure {

    @Autowired
    private StarterServiceProperties properties;

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = "itstack.door", value = "enabled", havingValue = "true")
    StarterService starterService() {
        return new StarterService(properties.getUserStr());
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = "itstack.door", value = "enabled", havingValue = "true")
    DoJoinPoint doJoinPoint() {
        return new DoJoinPoint();
    }

}

完成后编写一个spring.factories,导入这个AutoConfigure

org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.itstack.door.config.StarterAutoConfigure

修改一下pom,本地打个包

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
		
<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-jar-plugin</artifactId>
	<version>2.3.2</version>
	<configuration>
		<archive>
			<addMavenDescriptor>false</addMavenDescriptor>
			<index>true</index>
			<manifest>
				<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
				<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
			</manifest>
			<manifestEntries>
				<Implementation-Build>${maven.build.timestamp}</Implementation-Build>
			</manifestEntries>
		</archive>
	</configuration>
</plugin>			

在其他应用中导入

<dependency>
			<groupId>org.itatack.demo</groupId>
			<artifactId>door-spring-boot-starter</artifactId>
			<version>1.0.1-SNAPSHOT</version>
		</dependency>

编写配置

server:
  port: 9887

spring:
  application:
    name: demo

# 自定义中间件配置
itstack:
  door:
    enabled: true
    userStr: 1001,aaaa,ccc #白名单用户ID,多个逗号隔开

然后在导入这个中间件的应用中编写一个方法测试@DoDoor

@RestController
public class HelloWorldController {
    @Autowired
    private ApplicationContext applicationContext;

    @DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名单可访问用户拦截!\"}")
    @RequestMapping(path = "/user", method = RequestMethod.GET)
    public Map queryUserInfo(@RequestParam String userId) {
        Map<String, DoJoinPoint> beansOfType = applicationContext.getBeansOfType(DoJoinPoint.class);
        Map resultMap = new HashMap<>();
        resultMap.put("虫虫:" + userId, "天津市南开区旮旯胡同100号");
        return resultMap;
    }

}

测试结果

C:\Users\xxxx>curl http://localhost:9887/user?userId=1001132
{"code":"1111","info":"非白名单可访问用户拦截!"}
C:\Users\xxx>curl http://localhost:9887/user?userId=1111
{"code":"1111","info":"非白名单可访问用户拦截!"}
C:\Users\xx>curl http://localhost:9887/user?userId=1001
{"虫虫:1001":"天津市南开区旮旯胡同100号"}

源码是借鉴小傅哥的,感兴趣的读者可以参考:

Spring Boot 中间件开发(一)《服务治理中间件之统一白名单验证》

实践2-通用日志组件

需求介绍

微服务项目中,基于日志排查问题是非常重要的手段,而日志属于非功能范畴的一个职责,所以我们希望将日志打印和功能解耦。AOP就是非常不错的手段,但是在每个服务中都编写一个切面显然是非常不可取的。
所以我们希望通过某种手段会编写一个通用日志打印工具,只需一个注解即可实现对方法的请求响应进行日志打印。
所以我们这个例子仍然是利用自动装配原理编写一个通用日志组件。

实现步骤

  1. 创建日志插件模块cloud-component-logging-starter,并引入我们需要的依赖,如下所示,因为笔者要对spring-web应用进行拦截所以用到的starter-webaop模块,以及为了打印响应结果,笔者也用到hutool,完整的依赖配置如下所示:
 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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


        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
    </dependencies>
  1. 编写日志注解,如下所示,该注解的value用于记录当前方法要执行的操作,例如某方法上@SysLog("获取用户信息"),当我们的aop拦截到之后,就基于该注解的value打印该方法的功能。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SysLog {
    /**
     * 记录方法要执行的操作
     *
     * @return
     */
    String value();
}

  1. 编写环绕切面逻辑,代码如下所示,逻辑非常简单,拦截到了切面后若报错则打印报错的逻辑,反之打印正常请求响应结果。
@Aspect
public class SysLogAspect {

     private static Logger logger = LoggerFactory.getLogger(SysLogAspect.class);

    @Pointcut("@annotation(com.zsy.annotation.SysLog)")
    public void logPointCut() {

    }


    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        //类名
        String className = joinPoint.getTarget().getClass().getName();
        //方法名
        String methodName = signature.getName();

        SysLog syslog = method.getAnnotation(SysLog.class);
        //获取当前方法进行的操作
        String operator =syslog.value();

        long beginTime = System.currentTimeMillis();

        Object returnValue = null;
        Exception ex = null;
        try {
            returnValue = joinPoint.proceed();
            return returnValue;
        } catch (Exception e) {
            ex = e;
            throw e;
        } finally {
            long cost = System.currentTimeMillis() - beginTime;
            if (ex != null) {
                logger.error("业务请求:[类名: {}][执行方法: {}][执行操作: {}][耗时: {}ms][请求参数: {}][发生异常]",
                        className, methodName, operator, joinPoint.getArgs(), ex);
            } else {
                logger.info("业务请求:[类名: {}][执行方法: {}][执行操作: {}][耗时: {}ms][请求参数: {}][响应结果: {}]",
                        className, methodName, operator, cost, joinPoint.getArgs(), JSONUtil.toJsonStr(returnValue));
            }
        }

    }
}
  1. 编写配置类
@Configuration
public class SysLogAutoConfigure {

    @Bean
    public SysLogAspect getSysLogAspect() {
        return new SysLogAspect();
    }
}

  1. 新建spring.factories告知要导入Spring容器的类,内容如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.zsy.config.SysLogAutoConfigure
  1. 其他服务引入进行测试,以笔者为例,方法如下
@SysLog("获取用户信息")
    @GetMapping("getByCode/{accountCode}")
    public ResultData<AccountDTO> getByCode(@PathVariable(value = "accountCode") String accountCode) {
        log.info("远程调用feign接口,请求参数:{}", accountCode);
        return accountFeign.getByCode(accountCode);
    }

请求之后输出结果如下

2023-02-16 00:08:08,085 INFO  SysLogAspect:58 - 业务请求:[类名: com.zsy.order.controller.OrderController][执行方法: getByCode][执行操作: 获取用户信息][耗时: 892ms][请求参数: [zsy]][响应结果: {"data":{"accountCode":"zsy","amount":10000,"accountName":"zsy","id":1},"message":"操作成功","success":true,"status":100,"timestamp":1676477287856}]

参考文献

SpringBoot 自动装配原理

@Import、ImportSelector注解使用及源码分析

SpringBoot封装我们自己的Starter

Spring Boot 中间件开发(一)《服务治理中间件之统一白名单验证》

SpringCloud Alibaba微服务实战三十一 - 业务日志组件

Spring全解系列 - @Import注解

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

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

相关文章

Gitlab仓库推送到Gitee仓库的一种思路

文章目录 Gitlab仓库推送到Gitee仓库的一种思路1、创建Gitee的ssh公钥&#xff08;默认已有Gitlab的ssh公钥&#xff09;2、添加Gitlab远程仓库地址3、添加Gitee远程仓库地址4、拉取Gitlab远程仓库指定分支到本地仓库指定分支&#xff08;以test分支为例&#xff09;5、推送本地…

C++11特性:可调用对象以及包装器function的使用

在C中存在“可调用对象”这么一个概念。准确来说&#xff0c;可调用对象有如下几种定义&#xff1a; 是一个函数指针&#xff1a; int print(int a, double b) {cout << a << b << endl;return 0; } // 定义函数指针 int (*func)(int, double) &print…

【MAC】M2 安装mysql

一、docker下载地址 下载地址 二、安装docker完成 三、安装mysql 一、拉取镜像 # 拉取镜像 docker pull mysql# 或者 docker pull mysql:latest# 以上两个命令是一致的&#xff0c;默认拉取的就是 latest 版本的# 我们还可以用下面的命令来查看可用版本&#xff1a; docker…

[德人合科技]——设计公司 \ 设计院图纸文件数据 | 资料透明加密防泄密软件

国内众多设计院都在推进信息化建设&#xff0c;特别是在异地办公、应用软件资产规模、三维设计技术推广应用以及协同办公等领域&#xff0c;这些加快了业务的发展&#xff0c;也带来了更多信息安全挑战&#xff0c;尤其是对于以知识成果为重要效益来源的设计院所&#xff0c;防…

Docker单点部署[8.11.3] Elasticsearch + Kibana + ik分词器

文章目录 一、Elasticsearch二、Kibana三、访问四、其他五、ik分词器第一种&#xff1a;在线安装第二种&#xff1a;离线安装 Elasticsearch 和 Kibana 版本一般需要保持一致才能一起使用&#xff0c;但是从 8.x.x开始&#xff0c;安全验证不断加强&#xff0c;甚至8.x.x之间…

【每日一题】—— C. Largest Subsequence(Codeforces Round 915 (Div. 2))(规律、字符串处理)

&#x1f30f;博客主页&#xff1a;PH_modest的博客主页 &#x1f6a9;当前专栏&#xff1a;每日一题 &#x1f48c;其他专栏&#xff1a; &#x1f534; 每日反刍 &#x1f7e1; C跬步积累 &#x1f7e2; C语言跬步积累 &#x1f308;座右铭&#xff1a;广积粮&#xff0c;缓称…

制作一个多行时正确宽度的Textview,Android Textview 换行时宽度过长 右侧空白区域挤掉页面元素的解决方案

优化 Android 布局&#xff1a;创建自适应宽度的 TextView 引言 在Android应用开发中&#xff0c;布局优化是提升应用性能和用户体验的关键环节之一。特别是对于那些内容密集型的应用&#xff0c;如何高效地展示和管理文本内容成为了一个挑战。最近&#xff0c;在处理一个布局…

市场全局复盘 20231218

昨日回顾: SELECT CODE,成交额排名,净流入排名,代码,名称,DDE大单金额,涨幅,主力净额,DDE大单净量,CONVERT(DATETIME, 最后封板, 120) AS 最后封板,涨停分析,_3日涨幅百分比,连板天,封单额,封单额排名,DDE散户数量,总金额,获利盘 FROM dbo.全部A股20231218_ALL WHERE 连板天…

社交网络分析4(下):社交网络链路预测分析、LightGBM框架、LLSLP方法(LightGBM 堆叠链路预测)、堆叠泛化 、社交网络链路预测分析的挑战

社交网络分析4 写在最前面LightGBMLightGBM简介GBDT的核心概念和应用LightGBM的特点LightGBM与GBDT的比较 LightGBM的原理与技术GBDT的传统算法LightGBM的创新算法 GOSS&#xff08;Gradient-based One-Side Sampling&#xff09;算法解析概念和工作原理算法的逻辑基础GOSS算法…

PIC单片机项目(4)——基于PIC16F877A的温度光照检测装置

1.功能设计 基于PIC16F877A单片机&#xff0c;使用DS18B20进行温度测量&#xff0c;使用光敏电阻进行光照测量&#xff0c;将测量值实时显示在LCD1602屏幕上&#xff0c;同时可以设定光照阈值和温度阈值。当温度大于阈值&#xff0c;则蜂鸣器报警&#xff0c;当光照小于阈值&am…

EnvoyFilter API

目录 原文链接 https://onedayxyy.cn/docs/EnvoyFilter-API 本节实战 实战名称&#x1f6a9; 实战&#xff1a;EnvoyFilter API-全局范围-2023.12.18(测试成功)&#x1f6a9; 实战&#xff1a;EnvoyFilter API-配置优先级-2023.12.18(测试成功)&#x1f6a9; 实战&#xff1a…

开发企业展示小程序的关键步骤和技巧

随着移动互联网的快速发展&#xff0c;小程序已经成为企业展示形象、推广产品和服务的重要工具。拥有一个优秀的小程序可以帮助企业提高品牌知名度&#xff0c;吸引更多潜在客户&#xff0c;提升用户体验。以下是拥有一个展示小程序的步骤&#xff1a; 确定需求和目标 首先&am…

实时时钟(RTC)的选择与设计:内置晶体与外置晶体的优缺点对比

实时时钟(RTC)作为一种具备独立计时和事件记录功能的设备&#xff0c;现已广泛应用于许多电子产品中&#xff0c;并对时钟的精度要求越来越高。根据封装尺寸、接口方式、附加功能、时钟精度和待机功耗等因素进行分类&#xff0c;市场上有各种种类的RTC产品可供选择。 而在设计…

计网01 计算机网络基础

一、计算机网络基本概念 1、什么是计算机网络 网络&#xff1a;由两台或多台计算机通过网络设备串联&#xff08;网络设备通过传输介质串联&#xff09;而形成的网络网络设备&#xff1a;计算机、路由交换、防火墙、上网行为管理等传输介质&#xff1a;双绞线&#xff08;网线…

Unity中URP下的顶点偏移

文章目录 前言一、实现思路二、实现URP下的顶点偏移1、在顶点着色器中使用正弦函数&#xff0c;实现左右摇摆的效果2、在正弦函数的传入参数中&#xff0c;加入一个扰度值&#xff0c;实现不规则的顶点偏移3、修改正弦函数的振幅 A&#xff0c;让我们的偏移程度合适4、修改正弦…

使用特殊打字机键入单词的最少时间(贪心算法)

有一个特殊打字机&#xff0c;它由一个圆盘 和一个 指针组成&#xff0c; 圆盘上标有小写英文字母 a 到 z。只有 当指针指向某个字母时&#xff0c;它才能被键入。指针初始时指向字符 a 。 每一秒钟&#xff0c;你可以执行以下操作之一&#xff1a; 将指针顺时针或者逆时针移…

BearPi Std 板从入门到放弃 - 先天神魂篇(9)(RT-Thread DAC->ADC)

简介 RT-Thread DAC->ADC 使用, 就是DAC1输出模拟量, ADC1 读取模拟量转化成电压值, 基于开发板 &#xff1a; Bearpi Std(小熊派标准板)主芯片: STM32L431RCT6串口: Usart1DAC1: PA5, OUT2ADC1: PC2, IN3将板子上的E53 接口, 5 和 6用排线相连, 即实现内部DAC1->ADC1 …

TDDL笔记

TDDL分三层: Matrix层; 规则的管理 固定哈希算法&#xff0c;基本能保证数据均匀分布&#xff0c;它也是 TDDL 的默认路由算法。根据某个字段(如整形的 id 或者字符串的 hashcode)对分库的数量或者分表的数量进行取模&#xff0c;根据余数路由到对应的位置。一致性哈希算法&a…

【Docker】Docker安装部署maven私服

文章目录 镜像拉取构建nexus实例登录maven私服如何查看实例初始化的admin密码呢&#xff1f;1.查看容器挂载卷2.找到nexus_nexus_data查看挂载卷详情3.查看admin账号密码4.登录并重置密码 使用nexus私服1.设置settings.xml2.设置idea pom 出现的问题小插曲 镜像拉取 docker pu…

大数据Doris(三十八):Aggregate 和 Uniq 模型中的 ROLLUP

文章目录 Aggregate 和 Uniq 模型中的 ROLLUP 一、获得每个用户的总消费