1 | Spring:基础注解使用及场景概述

本来是打算按照书上面的内容,跟着看一看spring的实现。它是从XML中读取bean配置,感觉现在不怎么用XML来配置了,虽然可以跳过bean的读取,直接看bean的生命周期,但是毕竟现在基本上用注解,所以从网上找了一个直接基于注解讲解spring源码的视频。 视频内容讲解spring的AOP感觉很

本来是打算按照书上面的内容,跟着看一看spring的实现。它是从XML中读取bean配置,感觉现在不怎么用XML来配置了,虽然可以跳过bean的读取,直接看bean的生命周期,但是毕竟现在基本上用注解,所以从网上找了一个直接基于注解讲解spring源码的视频。

视频内容讲解spring的AOP感觉很好,至少第一遍的时候,看懂了一个大概。这篇博客在看第一遍的时候,记录了大概的内容,可以算是一个笔记;第二遍是在第一遍笔记的基础上,自己写代码进行实现、测试,并将代码和当时的想法再记录上来,有问题了再看视频。

概览

组件添加类注解

先看看比较基础的注解的用法。这个里面比较重要的注解有@Import,后面讲AOP经常用到,特别是在@Import中传ImportBeanDefinitionRegistrar的用法。因为AOP的入口点就是利用了这个注解。

@Configuration + @Bean

这是一个很常用的组合,在项目中也经常用到过。但是有一个地方需要注意,被@Bean修改的函数的函数名,默认情况下,会是返回bean的名字。要改名可以通过设置@Bean的name属性。

public class BasicMain {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicConfig.class);

//		System.out.println(applicationContext.getBean(Person.class));

		for (String definitionName : applicationContext.getBeanDefinitionNames()) {
			System.out.println(definitionName);
		}

	}
}

结果是:

这里有一个问题:为什么可以生成3个BeanDefinition,但是获取的时候,会报错失败?。先留给后面吧。

@Scope

spring默认情况下,bean是单例。可设置为多实例:prototype。如下:

@Configuration
public class BasicAnnotationConfig {

	@Bean
	@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
	public Person getPerson() {
		return new Person("萨日朗", 11);
	}

	@Bean
	@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
	public Actor getActor() {
		return new Actor();
	}

	@Bean
	public Programmer getProgrammer() {
		return new Programmer();
	}
}

然后创建IOC容器,然后对上面的3个bean都获取两次,比较两次获得的内容是否为同一对象。

public static void main(String[] args) {
	AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicAnnotationConfig.class);

	Person person1 = applicationContext.getBean(Person.class);
	Person person2 = applicationContext.getBean(Person.class);
	System.out.println("Person: " + (person1 == person2));

	Actor actor1 = applicationContext.getBean(Actor.class);
	Actor actor2 = applicationContext.getBean(Actor.class);
	System.out.println("Actor: " + (actor1 == actor2));

	Programmer programmer1 = applicationContext.getBean(Programmer.class);
	Programmer programmer2 = applicationContext.getBean(Programmer.class);
	System.out.println("Programmer: " + (programmer1 == programmer2));
}

结果为:

@ComponentScan

自动扫描包下的bean。

public class ComponentScanMain {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ComponentScanConfig.class);
		for (String definitionName : applicationContext.getBeanDefinitionNames()) {
			System.out.println(definitionName);
		}
	}
}

包、类的情况如下,其中dao和services下面的类,都是空类,没有任何其他数据,只有@Service和@Repository注解。

test-spring/src/main/java/
└── run
    └── oxffff
        └── spring
            ├── apps
            │   └── ComponentScanMain.java
            ├── configs
            │   └── ComponentScanConfig.java
            ├── dao
            │   └── PersonDao.java
            └── services
                ├── SpeakService.java
                └── TalkService.java

结果

@ComponentScan还可以指定excludeinclude属性,分别表示要排除的内容、扫描的内容。使用一个项目中用到的示例,效果就不演示了:

@ComponentScan(excludeFilters = {@ComponentScan.Filter(
    type = FilterType.ASSIGNABLE_TYPE,
    value = {com.***.service.AlgoProducer.AlgoProducerConfig.class,
        com.***.service.WsProducer.WsProducerConfig.class})
})

type还有好几种,分别是:

  • FilterType.ANNOTATION。注解类型
  • FilterType.ASSIGNABLE_TYPE。给定的类型
  • FilterType.ASPECTJ。AspectJ表达式
  • FilterType.REGEX。正则表达式
  • FilterType.CUSTOM。自定义过滤规则

如果指定的是自定义过滤规则,那么后面value表示的类,需要实现TypeFilter的match方法。

@Lazy

懒加载bean

@Conditional

条件加载bean。其中的value是一个数组类型,数组中的元素必须实现了Condition接口。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
	Class<? extends Condition>[] value();
}

Condition接口只有一个match方法,实现即可。

public interface Condition {
	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

matches方法的两个参数,可以用来做什么?简单理解,Spring Boot中有一个@ConditionalOnBean,基于Conditional,它的作用就是当容器中已经某些特定的bean时,加载某些bean。

另外,在Spring Boot中有很多基于@ConditionalXXXXX的注解,都是基于此注解。以@ConditionalOnProperty为例,项目中曾用到过此注解,解决一些关于不同环境的配置问题。

@Conditional(OnPropertyCondition.class)
public @interface ConditionalOnProperty {
	// ...
}

所以这个OnPropertyCondition一定是一个实现了Condition的类,可以从下面的继承继承结构中看出来:

@Import

快速导入bean,id默认是全类名。这个注解很重要,理解这个注解,对后面AOP的理解有很大的帮助。

普通类

不是很确定能不能导入,特地测试了一下,是可以import到IOC容器中的。

被@Configuration注解的类

实现了ImportSelector的类。此项返回的是一个类全名数组。

实现了ImportBeanDefinitionRegistrar的类。 手动注册Bean到IOC容器。

主要测试一次实现了ImportBeanDefinitionRegistrar接口的value值,后面用到的比较多。这里先实现一个如下:

public class MyInportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		registry.registerBeanDefinition("programmer", new RootBeanDefinition(Programmer.class));
	}
}

配置信息,主要是验证导入普通类、实现了ImportBeanDefinitionRegistrar的类。

@Configuration
@Import({Actor.class, MyInportBeanDefinitionRegistrar.class})
public class ImportConfig {
}

启动类为,主要功能是打印容器中的bean name:

public class ImportMain {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ImportConfig.class);
		for (String definitionName : applicationContext.getBeanDefinitionNames()) {
			System.out.println(definitionName);
		}
	}
}

结果导入了两个bean,如下:

可以看出来,import普通类时,默认的名称是类全名。

FactoryBean的使用

实现FactoryBean的类,然后用@Bean注解一个返回实现了该类的对象。

定义一个Actor的FactoryBean,并实现相应的方法。

public class ActorFactoryBean implements FactoryBean<Actor> {
	@Override
	public Actor getObject() throws Exception {
		return new Actor();
	}

	@Override
	public Class<?> getObjectType() {
		return Actor.class;
	}

	@Override
	public boolean isSingleton() {
		return true;
	}
}

在配置类中,生成相应的bean配置。

@Configuration
public class FactoryBeanConfig {
	@Bean
	public ActorFactoryBean getActorFactoryBean() {
		return new ActorFactoryBean();
	}
}

通过ActorFactoryBean的bean名称,获取bean,看得到的bean类型是什么,然后再加一个&符号,对比两次结果:

public class ActorFactoryBeanMain {
	public static void main(String[] args) {
		ApplicationContext applicationContext = new AnnotationConfigApplicationContext(FactoryBeanConfig.class);

		for (String definitionName : applicationContext.getBeanDefinitionNames()) {
			System.out.println(definitionName);
		}

		Object obj1 = applicationContext.getBean("getActorFactoryBean");
		System.out.println(obj1.getClass());

		Object obj2 = applicationContext.getBean("&getActorFactoryBean");
		System.out.println(obj2.getClass());
	}
}

容器中存在的bean是getObject()方法返回的对象,加&可获取factoryBean本身。

Bean的生命周期

主要参考了这篇博客,总结得很好。

流程图

分类

Bean的完整生命周期经历了各种方法调用,这些方法可以划分为以下几类:

Bean自身的方法:这个包括了Bean本身调用的方法和通过配置文件中bean的init-methoddestroy-method指定的方法。

Bean级生命周期接口方法:这个包括了BeanNameAwareBeanFactoryAwareInitializingBeanDiposableBean这些接口的方法。

容器级生命周期接口方法:这个包括了InstantiationAwareBeanPostProcessorBeanPostProcessor 这两个接口实现,一般称它们的实现类为“后处理器”。

工厂后处理器接口方法:这个包括了AspectJWeavingEnabler, ConfigurationClassPostProcessor, CustomAutowireConfigurer等等非常有用的工厂后处理器接口的方法。工厂后处理器也是容器级的。在应用上下文装配配置文件之后立即调用。

用法

通过@Bean的initMethoddestroyMethod分别指定初始化方法、销毁方法。

  • 对象创建完成、并赋值好,才调用initMethod
  • 单例bean会执行销毁方法,多实例bean不会执行销毁方法;
  • 单示例:在容器启动时创建对象;多实例:在每次获取时创建对象。
  • 初始化方法都执行

通过让Bean实现InitializingBeanDisposableBean来实现参与初始化、销毁。

InitializingBeanafterPropertiesSet方法在initMethod方法之前执行;

DisposableBeandestroy方法在destroyMethod之前执行。

通过@PostConstruct@PreDestroy注解来指定

这两个是J2EE中的注解,需要导入javax.annotation-api依赖,如下:

// https://mvnrepository.com/artifact/javax.annotation/javax.annotation-api
compile group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2'

@PostConstruct在bean的所有依赖都注入完成后,会调用。在InitializingBeanafterPropertiesSet方法之前执行(测试结果如此)。

@PreDestroyDisposableBeandestroy方法之前执行(测试结果如此)。

通过实现BeanPostProcessor方法来

这个接口太厉害了,数据校验、@Autowire注入等等都是通过此接口来实现的。它有两个函数,他们的执行时机如下所示:

示例

对上面4种情况,参考一些资料写了如下demo,验证其中的调用顺序。

测试bean

public class LifeCycleTestBean
		implements InitializingBean, DisposableBean,
		BeanFactoryAware, BeanNameAware {

	public LifeCycleTestBean() {
		System.out.println("构造器执行...");
	}

	public void init1() {
		System.out.println("bean init method -> init1...");
	}

	@PostConstruct
	public void init2() {
		System.out.println("PostConstruct...init2...");
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		System.out.println("InitializingBean...afterPropertiesSet...");
	}

	@Override
	public void destroy() throws Exception {
		System.out.println("DisposableBean...destroy...");
	}

	@PreDestroy
	public void destroy2() throws Exception {
		System.out.println("PreDestroy...destroy...");
	}

	@Override
	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
		System.out.println("BeanFactoryAware...setBeanFactory...");
	}

	@Override
	public void setBeanName(String name) {
		System.out.println("BeanNameAware...setBeanName...");
	}
}

bean后置处理器

public class MyBeanPostProcessor implements BeanPostProcessor {
	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		System.out.println("Before 【" + beanName + "】 initialization");
		return bean;
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		System.out.println("After 【" + beanName + "】 initialization");
		return bean;
	}
}

配置类

@Configuration
public class LifeCycleConfig {
	@Bean(initMethod = "init1", destroyMethod = "destroy2")
	public LifeCycleTestBean getLifeCycleTestBean() {
		return new LifeCycleTestBean();
	}

	@Bean
	public MyBeanPostProcessor getMyBeanPostProcessor() {
		return new MyBeanPostProcessor();
	}

	@Bean
	public Actor getActor() {
		return new Actor();
	}
}

启动类

public class LifeCycleMain {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
		System.out.println("容器创建完成...");
		LifeCycleTestBean lifeCycleBean = ac.getBean(LifeCycleTestBean.class);
		ac.close();
	}
}

结果。结论都写在上面了。

赋值

@Value

  1. 基本数值;@Value("洋芋")
  2. SpEL;@Value("#{20 - 2}")
  3. 配置文件取值。@Value("${server.port}")。可以用@PropertySource("classpath:/**.properties")指定外部配置文件

自动装配

Spring利用依赖注入(DI),完成对IOC容器中各个组件的依赖关系赋值。

@Autowired

Spring注解。 默认优先按类型去容器中找TestBean组件。

@Autowired
private TestBean testBean1;

如果有多个TestBean,默认情况下,会再按照属性名testBean1去匹配。此时也可以用@Qualifier指定想要的beanName。如下会加载beanName为testBean的bean。

@Qualifier("testBean")
@Autowired
private TestBean testBean1;

默认情况下,如果IOC容器中没有相应的bean,会报初始化错误。可以通过@Autowired注解中的required属性来设置,默认为true。即:

@Autowired(required = false)
private TestBean testBean1;

还可以通过@Primary来设置首选bean。

@Autowired标注在不同地方的区别:

  1. set方法上。从IOC容器中取bean作为参数。
  2. 构造器上。从IOC容器中去bean作为参数;一个参数时,可省略@Autowired。
  3. 参数上。从IOC容器中去bean作为参数。
  4. 被@Bean注解的函数的参数,可用@Autowired注入,可省略。

@Resource

Java规范注解。 默认按照组件名称进行装配,可以通过name属性修改。不支持@Primary、没有required属性。

@Inject

Java规范注解。 需要导入依赖javax.inject。与@Autowired类似,但是没有required属性。

环境相关

@Profile

它的作用是指定此Bean所对应的环境,不是该环境时,不加载此bean。环境可以通过spring.profiles.active属性来指定,也可以通过JVM参数:-Dspring.profiles.active=test来指定。

获取Spring容器底层组件

想要名为xxx的组件,实现xxxAware接口,最后交由xxxAwareProcessor

例如:获取ApplicationContext需要实现ApplicationContextAware,而对它的处理是通过ApplicationContextAwareProcessor来处理,ApplicationContextAwareProcessor即实现了BeanPostProcessor,也就是前面所遇到的那个后置处理器。

Spring AOP

基础概念

  1. 通知(Advice)具体做的工作。比如打印日志。
  2. 连接点(JoinPoint)可以进行扩展的一个时机。比如说调用方法时、修改一个字段时等,但spring只支持调用方法时
  3. 切点(PointCut)通知所要织入的具体位置。以调用方法这个连接点为例,方法有很多,不可能为所有的方法都加入通知那些被选中的方法就是切点
  4. 切面(Aspect)切点切面的集合。
  5. 织入(Weaving)将切面应用到目标对象并创建代理对象的过程。通俗理解,把目标对象装饰加强成加入通知的代理对象。有3种织入时期:编译期类加载期运行期(Spring AOP)。

使用步骤

导入aop模块,即spring-aspects依赖

compile project(":spring-aspects")

定义业务逻辑类。

public class Divider {
    public int div(int x, int y) {
        return x / y;
    }
}

定义切面类。

通知包括:①前置通知(@Before)、②后置通知(@After)、③返回通知(@AfterReturning)、④异常通知(@AfterThrowing)、⑤环绕通知(@Around),分别对应目标方法执行前、执行后、返回后、异常时。

@Aspect
public class DividerAspect {
    @Pointcut("execution(* run.oxffff.spring.beans.Divider.div(..))")
    public void pointcut() {
        
    }
    
    @Before("pointcut()")
    public void before() {
        System.out.println("Before div()");
    }

    @After("pointcut()")
    public void after() {
        System.out.println("After div()");
    }

    @AfterReturning("pointcut()")
    public void afterReturning() {
        System.out.println("After div() returning");
    }

    @AfterThrowing("pointcut()")
    public void afterThrowing() {
        System.out.println("After div() throwing");
    }
}

@Aspect一定要加,这个注解相当于一个开关,加上了才会进行织入操作。

在切面类的目标方法中标注何时在何处运行。

可使用@PointCut简化代码。切点的写法可参考:

切面类业务类都放入IOC容器,并且通过IOC容器获取业务类。

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
    @Bean
    public Divider getDivider() {
        return new Divider();
    }

    @Bean
    public DividerAspect getDividerAspect() {
        return new DividerAspect();
    }
}

必须使用@Aspect指明哪个bean将作为切面类。

在@Configuration注解标注的配置类中,开启注解@EnableAspectJAutoProxy

启动类

public class AopMain {
    public static void main(String[] args) {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AopConfig.class);
        Divider divider = ac.getBean(Divider.class);
        System.out.println(divider.div(1, 2));
        System.out.println(divider.div(1, 0));
    }
}

执行结果

注意:JoinPoint在切面方法中如果要注入,必须要放在第一个参数位上。为啥?深刻怀疑是用了args[0],哈哈。

Read more

Volcano 与 Kubernetes GPU 调度学习笔记

本笔记系统整理 Volcano 调度器、Kubernetes 调度框架、GPU Device Plugin、HAMi 等云原生 AI 调度领域的核心知识,适合用于学习、复习和工程实践参考。 目录 * 第一部分:Volcano 入门 * 1. Volcano 是什么 * 2. 安装与快速使用 * 3. 核心特性一览 * 第二部分:Volcano 整体架构 * 4. Volcano 解决的核心问题 * 5. 整体架构与数据流 * 6. 三层抽象模型 * 第三部分:Volcano 核心实现原理 * 7. Session 机制 * 8. Gang Scheduling 实现 * 9. Queue 与 DRF 公平调度

容器镜像(4):镜像的常用工具箱

容器镜像(4):镜像的常用工具箱

前几篇在讲多架构镜像时已经用过 skopeo 和 crane 做镜像复制,这篇系统整理这两个工具的完整能力,同时介绍几个日常操作镜像时同样好用的工具。 一、skopeo:不依赖 Daemon 的镜像瑞士军刀 skopeo 的核心价值是绕过 Docker daemon,直接与 Registry API 交互。上一篇用它做镜像复制和离线传输,但它的能力远不止于此。 1.1 安装 # Ubuntu / Debian sudo apt install -y skopeo skopeo --version # skopeo version 1.15.1 1.2 inspect:免拉取检查镜像元数据 docker inspect 需要先把镜像拉到本地,skopeo inspect 直接向 Registry

容器镜像(3):多架构镜像构建

容器镜像(3):多架构镜像构建

一、什么是多架构镜像 1.1 OCI Image Index 上一篇介绍了单平台镜像的结构:一个 Manifest 指向 Config 和若干 Layer blob。多架构镜像在此之上多了一层——OCI Image Index(也叫 Manifest List),是一个轻量的索引文件,把多个单平台 Manifest 组织在一起: $ docker manifest inspect golang:1.22-alpine { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests&

容器镜像(2):containerd 视角下的镜像

容器镜像(2):containerd 视角下的镜像

一、为什么需要了解 containerd 如果你只用 docker run 跑容器,从来不关心底层,那可以不了解 containerd。但如果你在用 Kubernetes,或者想真正理解"容器运行时"是什么,containerd 是绕不开的。 事实上,当你执行 docker run 的时候,containerd 早就在后台悄悄工作了——Docker 从 1.11 版本开始,就把核心运行时剥离出来交给 containerd 负责。 1.1 Docker 的架构演变 早期的 Docker(1.10 及之前)是一个"大一统"的单体程序:一个 dockerd