模仿Spring实现一个类管理容器

简介: 项目的初衷是独立作出一个成熟的有特色的IOC容器,但由于过程参考Spring太多,而且也无法作出太多改进,于是目的变为以此项目作为理解Spring的一个跳板,与网上的一些模仿Spring的框架不同,本项目主要是针对注解形式

概述

项目的初衷是独立作出一个成熟的有特色的IOC容器,但由于过程参考Spring太多,而且也无法作出太多改进,于是目的变为以此项目作为理解Spring的一个跳板,与网上的一些模仿Spring的框架不同,本项目主要是针对注解形式
地址是Thales

流程

在Spring中,一个bean的形成分三个大的阶段,

  1. bean的定义阶段(包含BeanDefinition的加载,解析,与注册)
  2. bean的实例化阶段(包含对象的创建,属性的注入)
  3. bean的初始化阶段(包含一些资源的初始化,譬如打开文件建立连接等等)

这只是大概的划分,关于BeanPostProcessor等后置处理并没有显式的提及.

类的设计

如果只想了解一个bean是怎么从生到死的,只需要一步步debug就好了,如果看不懂,就多debug几遍.可是如果想实现一个类似的容器,类的设计,职责的分配,接口的实现继承必然是要了解的(除非你想几个类做完所有的事)

以下是DefaultListableBeanFactory的类图

image-20210824190714575-16298032363171.png

是不是顶不住

我们再来看一张图

image-20210824191140329.png

第一张是Spring5.0的,第二张图是Spring0.9的,所以并没有必要在一开始就引入过多的设计复杂度

我们再来看一套对比图

image-20210824191726968.png

image-20210824191846497.png

哪一个是0.9的,哪一个是5.0的一目了然.

说这么多的目的,是说明我们没必要一开始就奔着最完善的目标去写,可以一步步来,一步步加入功能

实现简易IOC

众所周知,SpringIoC中最基本的就是BeanFactory

我们先定义一个BeanFactory接口

//暂时就给这一个方法
public interface BeanFactory {/*** 根据名字获取Bean实例* @param name* @return*/Object getBean(String name);
}

beanDefinition

由于是注解形式,我们不能再像xml那样给定一个资源文件再去解析了,而应该去扫描classPath下所有带有@Component的类,

这时候我们需要给定的参数就从文件路径变成了包路径,我们只需要扫描这个包及其子包内符合条件的类,并且将其转化为BeanDefinition再注册就好.执行这个功能的是ClassPathBeanDefinitionScanner这个类.在这一步,就已经和传统的流程有所区别了,我们会传入一个ResourceLoader去实现具体的扫描功能(即定位),但不会再有专门的类去处理解析这一步

public interface Resource {File getFile();String getFilename();String getFilePath();
}
//在最初设计的时候这个抽象类似乎没有用,但考虑到以后的扩展,还是先放在这
public abstract class AbstractResource implements Resource {@Overridepublic String getFilename() {return getFile().getName();}@Overridepublic String getFilePath() {return getFile().getPath();}
}
//这就是最终我们实例化bean时用到的Resource类,在Spring中并没有直接用,而是通过外观模式集成了一下成为RootBeanDefinition
public class ClassPathResource extends AbstractResource {private final String path;private ClassLoader classLoader;private Class<?> clazz;public ClassPathResource(String path, ClassLoader classLoader, Class<?> clazz) {this.path = path;this.classLoader = classLoader;this.clazz = clazz;}
}
public interface ResourceLoader {Resource getResource(String location);
}
//此类能够实现加载多个资源
public interface ResourcePatternResolver extends ResourceLoader {String CLASSPATH_ALL_URL_PREFIX = "classpath*:";List<? extends Resource> getResources(String location);
}//这个类就是正式用于扫描的类了
public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {private final ResourceLoader resourceLoader;public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader){this.resourceLoader = resourceLoader;}@Overridepublic Resource getResource(String location) {return resourceLoader.getResource(location);}
//在Spring中,是通过一层层方法的包装完成包名到路径的转换再到每个文件的扫描再转换为Resource,这里暂时就先一步到位,把具体实现放在工具类里@Overridepublic List<? extends Resource> getResources(String location) {Set<Class<?>> classes = ClassUtils.getClasses(location);List<ClassPathResource> classPathResources = new ArrayList<>();for (Class<?> clazz:classes) {classPathResources.add(new ClassPathResource("",clazz.getClassLoader(),clazz));}return classPathResources;}
}

但最后直接使用的并不是PathMatchingResourcePatternResolver

而是把他作为ClassPathBeanDefinitionScanner的一个属性,在这个类里调用.

我们得到了Resource,如何获得对应的BeanDefinition?

先考虑这样一个问题,什么样的类可以被注册BeanDefinition?

  1. 添加了@Component注解或者满足其他注册的条件
  2. 不是接口或者抽象类

所以我们可以单独抽象出一个方法 boolean isCandidateComponent(Class<?> clazz)来判断是否被注册

现在到了注册阶段,依旧秉持面向接口编程的理念,同时考虑到单一职责,我们把注册Bean定义单独抽象出来

public interface BeanDefinitionRegistry {void registerBeanDefinition(BeanDefinition beanDefinition);
}

上文说到Bean定义的定位,解析,注册都是在ClassPathBeanDefinitionScanner里完成的,于是BeanDefinitionRegistry自然也成为了ClassPathBeanDefinitionScanner的属性之一

于是ClassPathBeanDefinitionScanner构建完成了

public class ClassPathBeanDefinitionScanner {//负责具体的Resource定位private ResourcePatternResolver resourcePatternResolver;//负责BeanDefinition解析private BeanDefinitionRegistry registry;public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry,String...basePackage) {this.registry = registry;this.resourcePatternResolver = new PathMatchingResourcePatternResolver((ResourceLoader) registry);this.scan(basePackage);}public void scan(String...basePackages){doScan(basePackages);}void doScan(String[] basePackages){Set<BeanDefinition> beanDefinitions = new LinkedHashSet<>();for (String basePackage:basePackages) {Set<BeanDefinition> candidates = findCandidateComponents(basePackage);for(BeanDefinition candidate:candidates){beanDefinitions.add(candidate);registry.registerBeanDefinition(candidate);}}}//获取被注册的bean的集合private Set<BeanDefinition> findCandidateComponents(String basePackage) {Set<BeanDefinition> candidates = new LinkedHashSet<>();List<? extends Resource> resources = getResourcePatternResolver().getResources(basePackage);for(Resource resource:resources){if(resource instanceof ClassPathResource){ClassPathResource classPathResource = (ClassPathResource)resource;if(isCandidateComponent(classPathResource.getClazz())){AnnotationBeanDefinition beanDefinition = new AnnotationBeanDefinition();beanDefinition.setClazz(classPathResource.getClazz());beanDefinition.setBeanName(BeanUtils.generateBeanName(classPathResource.getClazz().getName()));candidates.add(beanDefinition);}}}return candidates;}private ResourcePatternResolver getResourcePatternResolver() {return this.resourcePatternResolver;}//判断是否被注册boolean isCandidateComponent(Class<?> clazz){Component declaredAnnotation = clazz.getDeclaredAnnotation(Component.class);return declaredAnnotation!=null&&!clazz.isInterface();};
}

实例化

在什么时候实例化?我们说,在调用getBean()而又没有现成的bean时进行实例化

public abstract class AbstractBeanFactory implements BeanFactory{@Overridepublic Object getBean(String beanName)}

对象创建

有两种方式,通过Jdk默认的反射实现,或者用cglib代理实现.

默认自然是无参构造,但是如果传入了参数,则需要根据参数的类型和数量去匹配对应的构造函数,用其去实例化

于是我们抽象出InstantiationStrategy作为实例化接口,两种实例化方法都需要实现这个接口,我们真正去用的时候只需要去调该接口的方法就好

public interface InstantiationStrategy {Object instantiate(BeanDefinition beanDefinition, String beanName, BeanFactory owner);
}
public class SimpleInstantiationStrategy implements InstantiationStrategy {
}

属性注入

字段值获取

有两种方式可以实现字段值获取

  1. 直接注解Autowired或者Value
  2. Value里面填的不是值而是占位符,那么就需要解析占位符去获取

我们通过Class对象获取所有字段,再通过遍历所有字段查找加在字段上的注解来获取(这仅仅只是Spring的一种注入方式)

 //处理@Autowired注解for(Field field:declaredFields){Autowired autowired = field.getDeclaredAnnotation(Autowired.class);if(autowired != null){pvs.add(new PropertyValue(field.getName(),new BeanReference(BeanUtils.generateBeanName(field.getType().getName()),field.getType())));}}//处理@Value注解for(Field field:declaredFields){Value value = field.getDeclaredAnnotation(Value.class);if(value != null){String value1 = value.value();pvs.add(new PropertyValue(field.getName(),value1));}}

字段值填充

获取字段值后通过反射填入相应的字段中

for(Field field:mbd.getBeanClass().getDeclaredFields()){field.setAccessible(true);if (field.getName().equals(propertiesValue.getName())&&field.getType().isAssignableFrom(newValue.getClass())) {field.set(bean,newValue);}}

初始化

调用指定的初始化方法,进行资源的初始化.,如何获取初始化方法?在xml模式中,只要加个标签即可,如果是注解模式,加个注解标识一下或者在某个注解上加个参数,代表初始化方法,这个还没有实现

功能填充

后置处理器添加

上面我们已经实现了一个可以进行依赖查找,依赖注入的Bean容器,让我们再回顾一下Spring的流程,我们少了些什么,最容易想到的应该就是后置处理器了,包括BeanFactoryPostProcessorBeanPostProcessor两种,前者对于beanFactory进行修改操作,后者对于bean进行修改操作,同样是面向接口编程

首先建立BeanPostProcessor

public interface BeanPostProcessor {Object postProcessBeforeInitialization(Object bean, String beanName);Object postProcessAfterInitialization(Object bean, String beanName) ;
}

就目前来看,有什么是需要BeanPostProcessor来做的呢?我们可以把之前对注解进行处理,获取注入属性的代码分离出来,专门用一个BeanPostProcessor去处理

所有自定义实现的BeanPostProcessor都需要继承这个接口,由于BeanPostProcessor的作用是处理其他的Bean,所以必须要在其他被处理的Bean实例化之前被创建出来.于是我们在finishBeanFactoryInitialization(beanFactory);之前添加registerBeanPostProcessors(beanFactory);用于实例化所有的BeanPostProcessor

而这些beanPostProcessor的重要程度是不同的,例如处理注解注入的BeanPostProcessor优先级就要比一般的BeanPostProcessor优先级要高,所以需要先实例化

Aware接口添加

其实现在我们已经可以完全的把一个对象交由IOC容器了,但此时这个对象与容器之间的关系是单向的,容器能够操作bean,但bean不能借助容器,为了解决此类问题,我们添加一个Aware接口作为标志接口,由各个更具体的Aware去继承他,并在实例化属性之后,初始化方法执行之完成相关容器属性的注入

事件监听器添加

监听器是观察者模式的一种实现

我们先定义以下几个基本接口

public interface ApplicationEventPublisher {/*** 发布事件* @param event*/void publishEvent(ApplicationEvent event);
}public interface ApplicationEventMulticaster {/*** 添加广播事件* @param event*/void multicastEvent(ApplicationEvent event);/*** 添加对于某个事件的监听器* @param listener*/void addApplicationListener(ApplicationListener listener);/*** 移除指定监听器* @param listener*/void removeApplicationListener(ApplicationListener listener);
}
public interface ApplicationListener <E extends ApplicationEvent> extends EventListener {/*** 监听特定事件* @param event*/void onApplicationEvent(E event);
}

具体调用流程为具体的listener被添加到广播器中,事件通过publisher统一发布,而publishEvent最后会调用 multicastEvent(ApplicationEvent event)方法,经过相应判断后由对应监听器做出相应操作.

如何判断这个监听器是否对该事件感兴趣?

我们事先实现的listener是有泛型的,我们可以通过这个泛型与传入的事件类型的关系来判断

public boolean supportEvent(ApplicationListener<ApplicationEvent> listener,ApplicationEvent event){//先获取Class对象Class<? extends ApplicationListener> listenerClass = listener.getClass();//获取其实现的所有接口(包括泛型信息)Type[] genericInterfaces = listenerClass.getGenericInterfaces();for (Type genericInterface:genericInterfaces){//判断是否为泛型接口if(genericInterface instanceof ParameterizedType){ParameterizedType parameterizedType = (ParameterizedType) genericInterface;//得到所有泛型参数Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();for(Type actualTypeArgument:actualTypeArguments){try {Class<?> aClass = Class.forName(actualTypeArgument.getTypeName());//判断感兴趣的事件类型是否与传入事件相同,或者是其父类if(aClass.isAssignableFrom(event.getClass())){return true;}} catch (ClassNotFoundException e) {e.printStackTrace();}}}}return false;}

FactoryBean添加

目前的Bean都是由BeanFactory来产生的,

我们用FactoryBean接口来标识这个产生Bean的特殊的Bean

循环依赖的解决

循环依赖是指A依赖于B的同时B依赖于A,解决方法为实例化与初始化分离,如果只考虑一般情况的话用两级缓存实际上就够了,

代码优化

实现简易AOP

如果从正统的AOP开始的话,随之而来的就是一堆概念,包括切点,通知一类

我们先看AOP要做什么

所以说AOP的核心就是动态代理,我们以Cglib为例来看看动态代理要怎么用

 Enhancer enhancer = new Enhancer();
//1. 为哪个类进行代理enhancer.setSuperclass(Buy.class);enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> {//2. 为该类的哪个方法进行代理if(method.getName().equals("buyOne")){//3. 代理究竟要做什么System.out.println("hello");}//4. 调用原有的对象methodProxy.invokeSuper(o,objects);return o;});
//5. 产生代理后的对象Buy o = (Buy)enhancer.create();

这就是动态代理最核心的功能,也是AOP的核心功能,AOP的最终目的是代码5,即产生一个代理对象,把这个代理对象交给IOC去管理

而为了达成这个目的,AOP框架需要做好代码1-4所需要做的事,一和二组合起来,成了JoinPoint,3叫做Advice,这两个组合起来就叫做Advisor,可不可以不分这些种类,就全写在一个或几个类里,当然可以,Spring0.9就是这么做的,但发展到如今,早已采用了这种划分方式.本项目也采用这种分类.

先从连接点说起,如何确定到底在哪里实现功能增强,无非是类与方法两个层次;

我们先定义ClassFilterMethodMacther两个接口

public interface ClassFilter {/*** 给定类型是否匹配* @param clazz* @return*/boolean matches(Class< ? > clazz);
}
public interface MethodMatcher {/*** 对应类的对应方法是否匹配* @param method* @param targetClass* @return*/boolean matches(Method method,Class< ? > targetClass);
}

这两个接口必然是组合起来使用的,于是我们用PointCut将其组合起来

public interface Pointcut {/*** 获取ClassFilter* @return*/ClassFilter getClassFilter();/*** 获取MethodMatcher* @return*/MethodMatcher getMethodMatcher();
}

接口只是定义了抽象功能,这些功能还要有具体的实现

我们默认用Java的正则去匹配方法名,以此构建出JdkRegexMethodMatcher

public class JdkRegexMethodPointcut implements MethodMatcher, Pointcut{private Pattern[] compiledPatterns = new Pattern[0];@Overridepublic ClassFilter getClassFilter() {return null;}@Overridepublic MethodMatcher getMethodMatcher() {return this;}@Overridepublic boolean matches(Method method, Class<?> targetClass) {String name = method.getName();for (Pattern pattern :compiledPatterns) {Matcher matcher = pattern.matcher(name);if(matcher.matches()){return true;}}return false;}//预编译private Pattern[] compilePatterns(String[] source) throws PatternSyntaxException {Pattern[] destination = new Pattern[source.length];for (int i = 0; i < source.length; i++) {destination[i] = Pattern.compile(source[i]);}return destination;}public void initPatternRepresentation(String[] patterns) throws PatternSyntaxException {this.compiledPatterns = compilePatterns(patterns);}
}

Spring中,并不是直接继承的MethodMatcher,考虑到正则的语法不同,额外做了一层抽象,但在此处省略掉了

JdkRegexMethodMatcher同时也实现了PointCut类,也就是说,现在切点已经准备好了

再来看Advice

由于考虑的可扩展点比较多,于是继承的层次也变的多了

public interface Advice {
}
public interface BeforeAdvice extends Advice{
}
public interface MethodBeforeAdvice extends BeforeAdvice{void before(Method method, Object[] args, Object target) throws Throwable;
}

现在Advice也定义完了,具体的实现我们交由用户去做

接下来就是整合成Advisor

public interface Advisor {Advice getAdvice();
}
public interface PointcutAdvisor extends Advisor{Pointcut getPointcut();
}
public abstract class AbstractPointcutAdvisor implements PointcutAdvisor{private Advice advice;@Overridepublic Advice getAdvice() {return advice;}public void setAdvice(Advice advice) {this.advice = advice;}
}

目前已经定义好了Advisor的功能

我们再实现这个接口

public class RegexMethodPointcutAdvisor extends AbstractPointcutAdvisor {JdkRegexMethodPointcut pointcut = new JdkRegexMethodPointcut();private String[] patterns;public RegexMethodPointcutAdvisor() {}public RegexMethodPointcutAdvisor(Advice advice) {setAdvice(advice);}public void setPattern(String pattern) {setPatterns(pattern);}public void setPatterns(String... patterns) {this.patterns = patterns;pointcut.initPatternRepresentation(patterns);}@Overridepublic Pointcut getPointcut() {return pointcut;}
}

RegexMethodPointcutAdvisor就整合了PointCut以及Advice,通过他,我们就可以确定在何处做何种增强.

现在的advisor可以完成检验一个类是否要被代理的功能,但是如果这个类需要被代理,advisor却无法保存这个类的对应信息

于是我们需要一个类将advisor与对应的代理类结合起来,这就是AdvisedSupport

public class AdvisedSupport {private  TargetSource targetSource;private List<MethodInterceptor> methodInterceptors = new ArrayList<>();private List<PointcutAdvisor> advisors = new ArrayList<>();public TargetSource getTargetSource() {return targetSource;}public void setTargetSource(TargetSource targetSource) {this.targetSource = targetSource;}public List<MethodInterceptor> getMethodInterceptor() {return methodInterceptors;}public void addMethodInterceptor(MethodInterceptor methodInterceptor) {this.methodInterceptors.add(methodInterceptor);}public List<PointcutAdvisor> getAdvisor() {return advisors;}public void addAdvisor(PointcutAdvisor advisor) {MethodBeforeAdviceInterceptor methodBeforeAdviceInterceptor = new MethodBeforeAdviceInterceptor();methodBeforeAdviceInterceptor.setAdvice((MethodBeforeAdvice) advisor.getAdvice());addMethodInterceptor(methodBeforeAdviceInterceptor);this.advisors.add(advisor);}
}

上类属性中的TargetSource便是真正持有代理对象信息的类

现在万事具备,只需要用Cglib去使用我们已经持有的信息就可以创建出新的类了

public class CglibAopProxy implements AopProxy{private final AdvisedSupport advised;public CglibAopProxy(AdvisedSupport advised) {this.advised = advised;}@Overridepublic Object getProxy() {Enhancer enhancer = new Enhancer();//1. 为哪个类进行代理enhancer.setSuperclass(advised.getTargetSource().getTargetClass());enhancer.setCallback(new DynamicAdvisedInterceptor(advised));//5. 产生代理后的对象return enhancer.create();}private static class DynamicAdvisedInterceptor implements MethodInterceptor {private final AdvisedSupport advised;public DynamicAdvisedInterceptor(AdvisedSupport advised) {this.advised = advised;}@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {CglibInvocation cglibInvocation = new CglibInvocation(method,objects,o,methodProxy);//2. 为该类的哪个方法进行代理for(PointcutAdvisor advisor: advised.getAdvisor()){if(advisor.getPointcut().getMethodMatcher().matches(method,advised.getTargetSource().getTargetClass())){//3. 代理究竟要做什么return advised.getMethodInterceptor().get(0).invoke(cglibInvocation);}}//4. 调用源方法return cglibInvocation.proceed();}}
}

将这份代码与最初使用cglib的代码比较,会发现过程几乎是一模一样.但是作为一个框架,应该尽可能的给用户以方便

于是我们需要一个Creator去把这一切都做好,他需要负责将AdvicePointCut组合成Advisor,再将AdvisorTargetSource组装成AdvisedSupport,再将AdvisedSupport交给Cglib动态代理,产生代理对象,而用户只需要编写Advice以及切入点表达式即可

功能演示

  1. 属性注入

    1. 基本类型
    2. 引用类型
    3. 循环依赖
  2. 容器感知
  3. FactoryBean生成对象
  4. AOP切面增强
  5. 自定义BeanPostProcessor

困难及解决

  1. 首先是设计上的问题
  2. FactoryBean的实现
  3. AOP与IOC的结合
  4. 字段的注入

原文链接

本文为阿里云原创内容,未经允许不得转载。 

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

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

相关文章

湖仓一体化的路,很多人都只走了一半

2022已至&#xff0c;如果回看2021&#xff0c;这一年无疑是数据的价值进一步体现的一年。数据应用场景不断丰富&#xff0c;从工业、交通、金融到制造&#xff0c;几乎无处不在。当然&#xff0c;数据价值的迅速提升也给开发者和相关企业带来了新的问题。数据量的爆发让存储成…

学术顶会再突破!计算平台MaxCompute论文入选国际顶会VLDB 2021

简介&#xff1a; VLDB 2021上&#xff0c;阿里云计算平台MaxCompute参与的论文入选&#xff0c;核心分布式调度执行引擎Fangorn、基于TVR Cost模型的通用增量计算优化器框架Tempura等分别被Industry Track、Research Track录取。 一、顶会概览 VLDB 2021上&#xff0c;阿里云…

技术干货 | 应用性能提升 70%,探究 mPaaS 全链路压测的实现原理和实施路径

简介&#xff1a; 全链路压测方案下&#xff0c;非加密场景下至少有 70% 的性能提升&#xff0c;加密场景下 10%的性能提升&#xff0c;并在 MGS 扩容完成后可实现大幅的性能提升&#xff0c;调优的结果远超预期。 业务背景 随着移动开发行业的步入存量时代&#xff0c;App 整…

投稿指南 | 云计算领域最前沿资讯、技术,期待您的专业解读!

我们是谁&#xff1f;CSDN云计算是CSDN旗下官方账号&#xff0c;提供云计算、大数据、虚拟化、数据中心、OpenStack、CloudStack、机器学习、智能算法等相关云计算观点、云计算技术、云计算平台、云计算实践、云计算产业咨询等服务。内容平台方面&#xff0c;我们的目标读者主要…

DataWorks 功能实践速览03期 — 生产开发环境隔离

简介&#xff1a; DataWorks功能实践系列&#xff0c;帮助您解析业务实现过程中的痛点&#xff0c;提高业务功能使用效率&#xff01; 往期回顾&#xff1a; DataWorks 功能实践速览01期——数据同步解决方案&#xff1a;为您介绍不同场景下可选的数据同步方案。DataWorks 功…

鸿蒙手表esim,鸿蒙手表终于来了!或将支持 eSIM,实现独立通话

原标题&#xff1a;鸿蒙手表终于来了&#xff01;或将支持 eSIM&#xff0c;实现独立通话根据此前的爆料消息&#xff0c;华为将于 6 月份带来与鸿蒙相关的产品发布会&#xff0c;备受瞩目的平板、手表等新品也将亮相。临近产品发布&#xff0c;华为官方也开始了新品的预热。今…

Pull or Push?监控系统如何选型

简介&#xff1a; 对于建设一套公司内部使用的监控系统平台&#xff0c;相对来说可选的方案还是非常多的&#xff0c;无论是用开源方案自建还是使用商业的SaaS化产品&#xff0c;都有比较多的可选项。但无论是开源方案还是商业的SaaS产品&#xff0c;真正实施起来都需要考虑如何…

k8s 集群居然可以图形化安装了?

作者 | 小碗汤来源 | 我的小碗汤今天分享一个可以图形化搭建k8s集群的项目&#xff0c;不妨试一试~本项目是基于 Kubespray 提供图形化的 K8S 集群离线安装、维护工具。Kubespray&#xff1a;https://github.com/kubernetes-sigs/kubesprayKuboard-SprayKuboard-Spray 是一款可…

poi excel导入 判断合并单元格_Excel合并单元格,你需要知道的那些事

合并单元格&#xff0c;是我们经常使用的一个功能。借助合并单元格功能&#xff0c;我们可以制作跨列表头&#xff0c;可以对数据进行显示上的分类&#xff0c;使数据看起来更加清晰明了&#xff0c;让我们的Excel表格看起来更加专业。找到菜单栏的合并单元格功能&#xff0c;我…

当设计模式遇上 Hooks

简介&#xff1a; 数据结构与设计模式能够指导我们在开发复杂系统中寻得一条清晰的道路&#xff0c;既然都说 Hooks 难以维护&#xff0c;那就尝试让「神」来拯救这混乱的局面。对于「设计模式是否有助于我们写出更优雅的 Hooks 」这个问题&#xff0c;看完本文&#xff0c;相信…

PostgreSQL数据目录深度揭秘

简介&#xff1a; PostgreSQL是一个功能非常强大的、源代码开放的客户/服务器关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;被业界誉为“先进的开源数据库”&#xff0c;支持NoSQL数据类型&#xff0c;主要面向企业复杂查询SQL的OLTP业务场景&#xff0c;提供…

深入浅出 Spring 架构设计

作者 | 三太子敖丙来源 | 敖丙前言为什么需要Spring? 什么是Spring?对于这样的问题&#xff0c;大部分人都是处于一种朦朦胧胧的状态&#xff0c;说的出来&#xff0c;但又不是完全说的出来&#xff0c;今天我们就以架构设计的角度尝试解开Spring的神秘面纱。本篇文章以由浅入…

海云健康:上云为10万家药店带去了什么价值?

“全国每5个人里,就有1个正在接受海云健康系统提供的服务。” 在海云健康(以下简称“海云”)的系统后台上,每一分钟就有10万笔的买药订单涌动。也许很多人没有听过海云健康的名字,但当他们走进社区药店时,已经在享受海云的“存健康”药店会员管理系统提供的服务。 海云创办于…

android系统手势app,8种iOS手势规定和14种android手势规定详解

不知道大家对ios系统和android系统的规定的原生手势有哪些吗&#xff1f;看到这样的标题&#xff0c;你能够回答出几个呢&#xff1f;其实&#xff0c;APP设计师和h5开发工程师对移动设备的手势的了解和理解是非常有必要的。只有掌握了这些平台的手势规定才能设计出符合用户操作…

mPaas 运维流程介绍

简介&#xff1a; 金融级移动开发平台 mPaaS&#xff08;Mobile PaaS&#xff09;为 App 开发、测试、运营及运维提供云到端的一站式解决方案&#xff0c;能有效降低技术门槛、减少研发成本、提升开发效率&#xff0c;协助企业快速搭建稳定高质量的移动应用。在我们日常运维过程…

360借条通过CCRC权威认证,再获国家级认可

近日&#xff0c;中国网络安全审查技术与认证中心&#xff08;CCRC&#xff09;向360借条App颁发移动互联网应用程序&#xff08;App&#xff09;安全认证证书。通过该认证&#xff0c;表明360借条App在个人信息保护方面的工作再次取得了国家级肯定。 随着移动互联的蓬勃发展&…

ElasticSearch IK 分词器快速上手

简介&#xff1a; ElasticSearch IK 分词器快速上手 一、安装 IK 分词器 1.分配伪终端 我的 ElasticSearch 是使用 Docker 安装的,所以先给容器分配一个伪终端.之后就可以像登录服务器一样直接操作docker 中的内容了docker exec -it 容器ID /bin/bash 2.使用 elasticsearch…

装完系统还要装什么_家里装了空调还要装空气净化系统吗?会不会太浪费了?...

微信搜一搜舒适11今天这篇文章&#xff0c;小壹就向大家科普一下空调和新风系统&#xff0c;告诉大家为什么装了空调还要装新风机。1、空调是什么&#xff1f; 对此大家都能够脱口而出&#xff1a;空调就是用来制冷或制热的机器&#xff0c;能够改变室内温度&#xff0c;让我们…

移动端性能优化系列—启动速度

简介&#xff1a; 移动端性能对用户体验、留存有着至关重要的影响&#xff0c;作为开发者是不是被这样吐槽过&#xff0c;“这个 APP 怎么这么大&#xff1f;”、“怎么一直在 APP 封面图转悠&#xff0c;点不进去”、“进入详情效果有些卡”、“用 4G 使用你们的 APP&#xff…

三重框架构建和威胁情报及时可达,山石网科发布StoneOS 5.5R9

升级的StoneOS 5.5R9版本&#xff0c;在预测与发现、防御与控制、检测与分析、响应与管理四个角度&#xff0c;通过云端运营中心的情报赋能和统筹运维&#xff0c;策略助手的访问链接发现&#xff0c;边界流量过滤的IP快速分类与阻断&#xff0c;精确边缘策略对用户与应用的精细…