目 录CONTENT

文章目录

【实践】Spring编码时的常见问题解析

FatFish1
2024-11-06 / 0 评论 / 0 点赞 / 84 阅读 / 0 字 / 正在检测是否收录...

问题1 - 给abstract类注解@Component会出现什么情况?

A:会将带有@Lookup注解方法的抽象类注册成bean

众所周知,abstract类是不能实例化的,但实际上spring注册bean与实例化是存在差异的。bean是被托管后的实例,尽管抽象类不能实例化,但是依旧还是可以有bean的。这部分可以参考代码ClassPathScanningCandidateComponentProvider#scanCandidateComponents

ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setSource(resource);
if (isCandidateComponent(sbd)) {
    ……
}

使用方法isCandidateComponent(sbd)判断该类是否是需要被扫描到注册成bean的类

继续分析源码

protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
    AnnotationMetadata metadata = beanDefinition.getMetadata();
    return (metadata.isIndependent() && (metadata.isConcrete() ||
          (metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))));
}

可以看到这里判断时需同时满足以下两个条件

  • 条件1 - metadata.isIndependent():是独立类,即非内部类

  • 条件2 - 满足以下两种场景之一:

    • metadata.isConcrete():是具体类,即非抽象类或接口

    • 同时满足以下两种场景:

      • metadata.isAbstract():是抽象类

      • metadata.hasAnnotatedMethods(Lookup.class.getName())且有方法被@Lookup注解

因此可以得知,abstract类是可以被注册成bean的,但前提条件是其中有方法被@Lookup注解

@Lookup标签的作用是将注解的方法重写为调用BeanFactory#getBean()方法,返回一个对应的实例bean,它的作用是实现单例中使用非单例,从而解决多线程产生的并发问题

看下面例子:

假设有一个在多线程状态下启动的service

// 配置spring环境
@Configuration
@ComponentScan("project.myproject.test")
public class ApplicaitonConfig {
}

// 模拟一个多线程执行的service
@Service
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = ApplicaitonConfig.class)
public class lookuptest {

    @Autowired
    private LookupComponent lookupComponent;

    @Test
    public void lookuptest() throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(lookupComponent.getTestClass().hashCode());
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(lookupComponent.getTestClass().hashCode());
            }
        }).start();
        TimeUnit.SECONDS.sleep(5);

    }
}

这个service在方法中起了两个线程,调用lookupComponent.getTestClass() 方法,看实现:

// 写法1
@Component
public class LookupComponent {
    
    @Autowired
    private TestClass testClass

    public TestClass getTestClass() {
        return testClass;
    }
}

// 写法2
@Component
public class LookupComponent {

    @Lookup
    public TestClass getTestClass() {
        return null;
    }
}

// 写法3
@Component
public abstract class LookupComponent {
    @Lookup
    public abstract TestClass getTestClass();
}

可以看到写法1是通过注入bean,返回bean,而写法2是通过注解@Lookup转换为调用BeanFactory#getBean() ,写法3是放弃了@Lookup方法中的空实现改完使用抽象类

再看TestClass的实现:

// 写法A
@Component
public class TestClass {
}

// 写法B
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class TestClass {
}

写法B相比于写法A,增加了@Scope注解决定B为非单例模式

在这个测试场景下,可以实现非单例获取的为{写法2x写法B},{写法3x写法B},两个场景,原因如下:

  • 使用写法A,TestClass为非单例,每次使用BeanFactory#getBean()从三级缓存中获取,拿到都是同一个bean,因此不能获取非单例bean

  • 写法1,在bean加载阶段就已经使用BeanFactory#getBean()获取了bean并且完成了注入,并且BeanFactory#getBean()方法有且仅有1次调用,因此拿到的也是单例bean

而写法3相比于写法2,就是省略了return null;这种坏味道代码,也就是本问题之前的疑问,abstract+@Lookup可以被注册为bean的设计理念所在

问题2 - 对于由spring托管的子类,spring如何处理继承对象的注入?

A:对于spring托管的实现类,其父类在没有实例化需求时一般不使用@Service/@Component注解(例如抽象父类,用了也白用,除非是问题1的场景),但是其成员变量可以使用@Autowired注入,子类在完成populate工作时,会向上寻找其父类实现中具有@Autowired注解的成员变量进行注入

当一个子类的父类无需被spring托管,或父类为抽象类无需实例化时,子类未重写的方法调用到父类的成员变量时,可以借助父类的@Autowired注解进行注入

看一个案例:

// 父类实现
public class AbstractFather {

    @Autowired
    private EatService eatService;

    public void whoEat(String name) {
        System.out.println(String.format(eatService.eat(), name));
    }
}

// 子类实现
@Component
public class Son extends AbstractFather{

}

// service实现
@Service
public class EatService {
    public String eat() {
        return "%s is eating";
    }
}

// 配置spring环境
@Configuration
@ComponentScan("project.myproject.test")
public class ApplicaitonConfig {
}

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = ApplicationConfig.class)
public class ExtendTest {
    @Autowired
    private Son son;
    @Test
    public void test() {
        son.whoEat("son");
    }
}

这里在调用son.whoEat() 时,可知调用到父类的方法,使用的是父类的eatService

如果子类重写了父类的whoEat()方法,就必须自己声明eatService变量,同时自行完成注入,看下案例:

@Component
public class Son extends AbstractFather{
    
    @Autowired
    private EatService eatService;

    @Override
    public void whoEat(String name) {
        System.out.println(String.format(eatService.eat(), "name no way"));
    }
}

对于此问题的源码在AbstractAutowireCapableBeanFactory#populateBean这个方法是创建bean实例三步中的第二步,作用的bean的属性注入

// AbstractAutowireCapableBeanFactory#populateBean
for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) {
	PropertyValues pvsToUse = bp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);

这里是通过InstantiationAwareBeanPostProcessor#postProcessProperties的实现类找属性注入的入口,处理@Autowired注解的实现类和方法是是AutowiredAnnotationBeanPostProcessor#postProcessProperties

public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
    InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);

// AutowiredAnnotationBeanPostProcessor#findAutowiringMetadata
metadata = buildAutowiringMetadata(clazz);

继续跟进

// org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#buildAutowiringMetadata
do {
    // 模块1、反射出所有field,找到带有@Autowired方法的
	final List<InjectionMetadata.InjectedElement> fieldElements = new ArrayList<>();
	ReflectionUtils.doWithLocalFields(targetClass, field -> {
		MergedAnnotation<?> ann = findAutowiredAnnotation(field);
		if (ann != null) {
			if (Modifier.isStatic(field.getModifiers())) {
				if (logger.isInfoEnabled()) {
					logger.info("Autowired annotation is not supported on static fields: " + field);
				}
				return;
			}
			boolean required = determineRequiredStatus(ann);
			fieldElements.add(new AutowiredFieldElement(field, required));
		}
	});

    // 模块2、反射出所有method,找到带有@Autowired方法的,例如set方法
	final List<InjectionMetadata.InjectedElement> methodElements = new ArrayList<>();
	ReflectionUtils.doWithLocalMethods(targetClass, method -> {
		Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
		if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
			return;
		}
		MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
		if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
			if (Modifier.isStatic(method.getModifiers())) {
				if (logger.isInfoEnabled()) {
					logger.info("Autowired annotation is not supported on static methods: " + method);
				}
				return;
			}
			if (method.getParameterCount() == 0) {
				if (logger.isInfoEnabled()) {
					logger.info("Autowired annotation should only be used on methods with parameters: " +
							method);
				}
			}
			boolean required = determineRequiredStatus(ann);
			PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
			methodElements.add(new AutowiredMethodElement(method, required, pd));
		}
	});
	elements.addAll(0, sortMethodElements(methodElements, targetClass));
	elements.addAll(0, fieldElements);
	targetClass = targetClass.getSuperclass();
}
while (targetClass != null && targetClass != Object.class);

可以看到方法主体是一个do-while框架,每次循环分别找被@Autowired注解的field和method,但是找field的时候是找不到父类的,因为里面反射的时候使用的是clazz.getDeclaredFields() 方法

public static void doWithLocalFields(Class<?> clazz, FieldCallback fc) {
	for (Field field : getDeclaredFields(clazz)) {

// -----------------------------------------------------------------------------------------------------------

private static Field[] getDeclaredFields(Class<?> clazz) {
	Assert.notNull(clazz, "Class must not be null");
	Field[] result = declaredFieldsCache.get(clazz);
	if (result == null) {
		try {
			result = clazz.getDeclaredFields();
			declaredFieldsCache.put(clazz, (result.length == 0 ? EMPTY_FIELD_ARRAY : result));
		}
		catch (Throwable ex) {
			throw new IllegalStateException("Failed to introspect Class [" + clazz.getName() +
					"] from ClassLoader [" + clazz.getClassLoader() + "]", ex);
		}
	}
	return result;
}

真正能找到父类是因为最后一行逻辑

targetClass = targetClass.getSuperclass();

每次获取其父类,继续循环,指导父类为null,或父类为Object.class,停止循环

这部分可以配合spring-beans创建流程看:

http://www.chymfatfish.cn/archives/spring-beansgetfromfactory#postprocessmergedbeandefinition

问题3 - spring如何处理被@Component注解的内部类

A:内部类也使用@Component注解,外部类也必须被spring托管(例如使用@Component注解),这样内部类可以被spring托管成bean,且不关心内部类使用哪种关键词修饰

这个问题要从ApplicationContext的refresh流程开始看,在AbstractApplicationContext#refresh 方法中有一步调用:

// --- org.springframework.context.support.AbstractApplicationContext#refresh ---

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

调用invokeBeanFactoryPostProcessors激活beanDefinition加载前的前置处理器

protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
    PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
    ……
}

它的核心代码是两个部分:通过getBeanFactoryPostProcessors()方法获取到通过AbstractApplicationContext#addBeanFactoryPostProcessor 方法硬编码的前置处理器,如何调用PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors 激活前置处理器

public static void invokeBeanFactoryPostProcessors(
			ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {

		Set<String> processedBeans = new HashSet<>();
        
        // part1. if-else 部分 激活硬编码的前置处理器
		if (beanFactory instanceof BeanDefinitionRegistry) {
			……
		}

		else {
			// Invoke factory processors registered with the context instance.
			invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
		}

        // part2. 激活注册的前置处理器
		……

补充前置处理器的分类:

  • 硬编码的:通过AbstractApplicationContext#addBeanFactoryPostProcessor 方法添加进来的

  • 注册的:通过PriorityOrdered接口、Ordered接口、No-Ordered没有实现排序接口的处理器

  • BeanFactoryPostProcessor和BeanDefinitionRegistryPostProcessor两类,二者均可以采用硬编码的,也可以采用注册的

可以看的invokeBeanFactoryPostProcessors的主体框架:

  • part1部分是一个if-else,条件BeanFactory是否是BeanDefinitionRegistry的实现

    • 如果是,走if,处理硬编码进来的BeanFactoryPostProcessors处理器和硬编码的+注册进来的BeanDefinitionRegistryPostProcessor处理器

    • 如果否,走else,只处理硬编码进来的BeanFactoryPostProcessors处理器

  • part2部分省略掉了,是处理注册进来的BeanFactoryPostProcessors

因为默认情况下使用的BeanFactory是DefaultListableBeanFactory,它实现了BeanDefinitionRegistry,因此在part1判断的时候走if路线

public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
       implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable

看if中的逻辑

BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
List<BeanFactoryPostProcessor> regularPostProcessors = new ArrayList<>();
List<BeanDefinitionRegistryPostProcessor> registryProcessors = new ArrayList<>();

首先预置了两个List,就是为了区分BeanFactoryPostProcessors和BeanDefinitionRegistryPostProcessor两类处理器

然后处理硬编码的部分,即方法入参进来的处理器,判断属于哪一类,分别存入对应的list

for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
    if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
       BeanDefinitionRegistryPostProcessor registryProcessor =
             (BeanDefinitionRegistryPostProcessor) postProcessor;
       registryProcessor.postProcessBeanDefinitionRegistry(registry);
       registryProcessors.add(registryProcessor);
    }
    else {
       regularPostProcessors.add(postProcessor);
    }
}

然后找注册进来的BeanDefinitionRegistryPostProcessor处理器

第一部分是实现了PriorityOrdered接口的BeanDefinitionRegistryPostProcessor处理器

// First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered.
String[] postProcessorNames =
       beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
    if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
       currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
       processedBeans.add(ppName);
    }
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
currentRegistryProcessors.clear();

在这里会找到一个专门处理注解的处理器ConfigurationClassPostProcessor,然后激活它的postProcessBeanDefinitionRegistry方法

private static void invokeBeanDefinitionRegistryPostProcessors(
       Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry, ApplicationStartup applicationStartup) {
    for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
       StartupStep postProcessBeanDefRegistry = applicationStartup.start("spring.context.beandef-registry.post-process")
             .tag("postProcessor", postProcessor::toString);
       postProcessor.postProcessBeanDefinitionRegistry(registry);
       postProcessBeanDefRegistry.end();
    }
}

向下跟踪调用到ConfigurationClassPostProcessor#processConfigBeanDefinitions方法

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
	List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
	String[] candidateNames = registry.getBeanDefinitionNames();
    // 依次遍历候选类,找到非@Configuration的类,存入configCandidates中等待实例化
	for (String beanName : candidateNames) {
		BeanDefinition beanDef = registry.getBeanDefinition(beanName);
		if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
			if (logger.isDebugEnabled()) {
				logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
			}
		}
		else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
			configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
		}
	}

这里首先找到非@Configuration的类,存入configCandidates中等待实例化,后面又候选类的判断和排序,跳过,直接看实例化的部分

ConfigurationClassParser parser = new ConfigurationClassParser(
		this.metadataReaderFactory, this.problemReporter, this.environment,
		this.resourceLoader, this.componentScanBeanNameGenerator, registry);

Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
do {
    StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse");
    parser.parse(candidates);

调用ConfigurationClassParser#parse方法进行实例化,这里一路向下跟踪,调用到ConfigurationClassParser#doProcessConfigurationClass 方法,该方法处理了@Component、@PropertySource、@ComponentScan、@Import、@ImportResource、@Bean几种注解

if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
	// Recursively process any member (nested) classes first
	processMemberClasses(configClass, sourceClass, filter);
}

对于@Component注解,首先要处理它的内部类,调用来到ConfigurationClassParser#processMemberClasses方法

private void processMemberClasses(ConfigurationClass configClass, SourceClass sourceClass,
       Predicate<String> filter) throws IOException {
    // 找到内部类
    Collection<SourceClass> memberClasses = sourceClass.getMemberClasses();
    if (!memberClasses.isEmpty()) {
       List<SourceClass> candidates = new ArrayList<>(memberClasses.size());
       for (SourceClass memberClass : memberClasses) {
          // 对内部类进行校验,通过校验,加入到候选类中进行实例化
          if (ConfigurationClassUtils.isConfigurationCandidate(memberClass.getMetadata()) &&
                !memberClass.getMetadata().getClassName().equals(configClass.getMetadata().getClassName())) {
             candidates.add(memberClass);
          }
       }
       OrderComparator.sort(candidates);
       for (SourceClass candidate : candidates) {
          if (this.importStack.contains(configClass)) {
             this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
          }
          else {
             this.importStack.push(configClass);
             try {
                processConfigurationClass(candidate.asConfigClass(configClass), filter);
             }
             finally {
                this.importStack.pop();
             }
          }
       }
    }
}

可以看到这里有两处可能被校验掉的条件:

  • 找内部类的getMemberClasses方法

  • 校验的isConfigurationCandidate方法

先看getMemberClasses方法

public Collection<SourceClass> getMemberClasses() throws IOException {
    ……
          Class<?>[] declaredClasses = sourceClass.getDeclaredClasses();
          List<SourceClass> members = new ArrayList<>(declaredClasses.length);
          for (Class<?> declaredClass : declaredClasses) {
             members.add(asSourceClass(declaredClass, DEFAULT_EXCLUSION_FILTER));
          }
          return members;
       }
       ……
    }

可以看到是直接通过反射getDeclaredClasses()方法获取的,因此不管什么private、protect、public、static关键词,都是可以拿到的

然后用DEFAULT_EXCLUSION_FITER执行了一次过滤

private static final Predicate<String> DEFAULT_EXCLUSION_FILTER = className ->
       (className.startsWith("java.lang.annotation.") || className.startsWith("org.springframework.stereotype."));

再看isConfigurationCandidate 方法

public static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
    // Do not consider an interface or an annotation...
    if (metadata.isInterface()) {
       return false;
    }
    // Any of the typical annotations found?
    for (String indicator : candidateIndicators) {
       if (metadata.isAnnotated(indicator)) {
          return true;
       }
    }
    // Finally, let's look for @Bean methods...
    return hasBeanMethods(metadata);
}

// ------- ConfigurationClassUtils -----------------------------------------
private static final Set<String> candidateIndicators = new HashSet<>(8);
static {
	candidateIndicators.add(Component.class.getName());
	candidateIndicators.add(ComponentScan.class.getName());
	candidateIndicators.add(Import.class.getName());
	candidateIndicators.add(ImportResource.class.getName());
}

条件有三个:

  • 不允许是内部接口

  • 如果包含@Component、@ComponentScan、@Import、@ImportResource中任意即可

  • 不包含上述四个注解,必须有@Bean注解的方法在其中

到这里结论就很明显了,@Component注解的内部类,外部类也必须被spring托管,这样内部类可以加载成bean

问题4 - spring如何处理classpath:和classpath*:配置文件

A:通过PathMatchingResourcePatternResolver基于ClassLoader处理classpath*这类配置

对于classpath:和classpath*:的区别是:

  • classpath:只会到你的class路径中查找找文件。

  • classpath*:不仅包含class路径,还包括jar文件中(class路径)进行查找

对于classpath:的解析,在spring的默认解析器DefaultResourceLoader中

这里重点看下classpath*:是如何解析的,是在PathMatchingResourcePatternResolver中

其中使用的底层逻辑是基于classLoader获取所有的类路径,最常见的有两种:

  • 文件系统中的目录/project/target/classes/

  • JAR文件jar:file:/home/user/.m2/repository/com/example/lib.jar!/

此外,spring还支持从压缩包中解析等等

举个例子,找所有的META-INF/spring.factories文件

// 打印实际匹配的资源
Resource[] res = new PathMatchingResourcePatternResolver()
        .getResources("classpath*:META-INF/spring.factories");
Arrays.stream(res).map(r -> {
    try { return r.getURL(); } 
    catch (IOException e) { return e.toString(); }
}).forEach(System.out::println);

// Found: jar:file:/lib/spring-core-6.1.5.jar!/META-INF/spring.factories
// Found: jar:file:/lib/spring-web-6.1.5.jar!/META-INF/spring.factories

PathMatchingResourcePatternResolver的逻辑框架在spring这边已经看过了

http://www.chymfatfish.cn/archives/springxml#pathmatchingresourcepatternresolver

这次针对jar类型向下看看:

首先是基于classloader获取全部类路径:

// org.springframework.core.io.support.PathMatchingResourcePatternResolver#findPathMatchingResources
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
    String rootDirPath = determineRootDir(locationPattern);
    String subPattern = locationPattern.substring(rootDirPath.length());
    Resource[] rootDirResources = getResources(rootDirPath);

在getResoruces方法中,继续向下:

// org.springframework.core.io.support.PathMatchingResourcePatternResolver#getResources
public Resource[] getResources(String locationPattern) throws IOException {
    Assert.notNull(locationPattern, "Location pattern must not be null");
    if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
       ……
          // all class path resources with the given name
          return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
       }
    }
    ……

如果是以classpath*:开头的,跟进findAllClassPathResources方法

// org.springframework.core.io.support.PathMatchingResourcePatternResolver#findAllClassPathResources
protected Resource[] findAllClassPathResources(String location) throws IOException {
    String path = location;
    if (path.startsWith("/")) {
       path = path.substring(1);
    }
    Set<Resource> result = doFindAllClassPathResources(path);
    if (logger.isTraceEnabled()) {
       logger.trace("Resolved classpath location [" + location + "] to resources " + result);
    }
    return result.toArray(new Resource[0]);
}

// org.springframework.core.io.support.PathMatchingResourcePatternResolver#doFindAllClassPathResources
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
	Set<Resource> result = new LinkedHashSet<>(16);
	ClassLoader cl = getClassLoader();
	Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
	while (resourceUrls.hasMoreElements()) {
		URL url = resourceUrls.nextElement();
		result.add(convertClassLoaderURL(url));
	}
	if (!StringUtils.hasLength(path)) {
		// The above result is likely to be incomplete, i.e. only containing file system references.
		// We need to have pointers to each of the jar files on the classpath as well...
		addAllClassLoaderJarRoots(cl, result);
	}
	return result;
}

可以在doFindAllClassPathResources方法中找到,首先获取到classloader,然后直接cl.getResoruces(path) 方法获取到所有的URL格式

然后看下对jar类型的专门处理:

// org.springframework.core.io.support.PathMatchingResourcePatternResolver#doFindPathMatchingJarResources
String urlFile = rootDirURL.getFile();
try {
    int separatorIndex = urlFile.indexOf(ResourceUtils.WAR_URL_SEPARATOR);
    if (separatorIndex == -1) {
       separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
    }
    if (separatorIndex != -1) {
       jarFileUrl = urlFile.substring(0, separatorIndex);
       rootEntryPath = urlFile.substring(separatorIndex + 2);  // both separators are 2 chars
       jarFile = getJarFile(jarFileUrl);
    }
    else {
       jarFile = new JarFile(urlFile);
       jarFileUrl = urlFile;
       rootEntryPath = "";
    }
    closeJarFile = true;
}
catch (ZipException ex) {

这里可以就是分解jar的头尾,其中public static final String JAR_URL_SEPARATOR = "!/" 即上面看到的jar格式中的结尾

以一个场景为例,想要读取jar包中的配置文件/etc/configuration.json

可以通过如下逻辑:

String fileName = "etc/configuration.json";
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolv
Resource[] roots = resolver.getResources("classpath*:/**/" + fileName);
BufferedInputStream bufferedInputStream = new BufferedInputStream(formatFileUri.get(0).getInputStream());
String fileContent = IOUtils.toString(bufferedInputStream, Charset.defaultCharset());

这里比较特殊的一点是:jar包中的文件拿到Resource,可以拿到URL、URI,但是不能直接通过getFile获取文件,而apache.IOUtils则可以基于输入流读取到其中的文件

问题5-SpringMVC架构中的Mapping、Adapter、Handler到底是什么关系

A:Handler是将request处理成ModelAndView的工具;Mapping是请求url到Handler之间的映射;Adapter是不同种类的Handler都能统一被收集调用的适配器

首先可以看下SpringMVC部分的源码

http://www.chymfatfish.cn/archives/spring-mvc

springMVC的工作流程总结起来,如下图:

补链接图https://blog.csdn.net/zxd1435513775/article/details/103000992

总结这个流程就是:

  • 通过handler映射器找到对应的handler,返回handlerChain

  • 找到对应的adapter,通过adapter调用handler,返回ModelAndView

  • 通过ViewResolver解析视图返回

  • 渲染视图返回视图

可见其中的核心部分就是Mapping映射器、Adapter适配器、Handler处理器三个部分

首先Handler其实就是处理请求的核心类,一共有三种定义形式:

  • 使用@RequestMapping注解定义Handler,可以看到,使用@RequestMapping注解写起来比较方便,不需要自己从request中取参数,也不需要自己包装返回

// @RequestMapping加到类上即模块命名地址,访问该类下的所有方法都需要有一个地址前缀 /test+具体方法的path/value才能访问
@Controller
@RequestMapping("test")
public class RequestMappingController {
    // @RequestMapping写在方法上,则访问/test/testMapping访问
	@RequestMapping("testMapping")
    public String testMapping(){
    	return "requestmappingsuccess";//视图解析器前后缀拼接返回页面地址
	}
}

// 添加method属性指定方法,否则四种方法都匹配
//设置只支持post请求
@RequestMapping(path="testMethod",method = {RequestMethod.POST})
//设置只接收get请求
@RequestMapping(path="testMethod",method = {RequestMethod.GET})

// params属性指定request中必须包含某些参数值,包含才让该方法处理请求
//请求中必须要有参数username(页面中的name值),且参数值要为striveday才执行该方法
@RequestMapping(value="/testParam", params="username = strivedaay")
//请求中必须要有参数username(页面中的name值),且参数值不为striveday才执行该方法
@RequestMapping(value="/testParam", params="username != strivedaay")

// headers属性指定某些headers属性才能访问
@RequestMapping(value="testHeaders",headers={"context-type=text/plain","context-type=text/html"})
public String testHeaders(){}

// consumes属性指定Content-Type
@Controller
@RequestMapping(value = "/pets", method = RequestMethod.POST, consumes="application/json")
public void addPet(@RequestBody Pet pet, Model model) {    
    // implementation omitted
}

// produces属性使方法仅处理request请求中Accept头中包含了"application/json"的请求,同时暗示了返回的内容类型为application/json;
@Controller
@RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET, produces="application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId, Model model) {    
    // implementation omitted
}
// 配合@RequestParam设置请求参数,解析url中?后面的参数
// 通过@RequestParam让页面传入的参数username赋值给方法参数列表的name
@RequestMapping("testRequestParam")
    public String getUsername(@RequestParam("username") String name){
    	System.out.println("username = " + name);
 }

// 配合@RequesBody解析请求体中的参数,会自动将请求体中的参数以JSON形式解析成对应DTO
@RequestMapping("testRequestBody")
    public String getUsername(@RequestBody User user){
    	System.out.println("username = " + user.getName());
 }
  • 实现org.springframework.web.servlet.mvc.Controller控制器接口,此接口只有一个方法handleRequest(),用于请求的处理,返回ModelAndView

@Component("/home")
public class HomeController implements Controller {

	@Override
    public ModelAndView handleRequest(HttpServletRequest request,
    				 HttpServletResponse response) throws Exception {
        System.out.println("home ...");
        response.getWriter().write("home controller from body");
        return null; // 返回null告诉视图渲染  直接把body里面的内容输出浏览器即可
    }
}
  • 实现org.springframework.web.HttpRequestHandler接口,HttpRequestHandler用于处理Http requests,其类似于一个简单的Servlet,只有一个handlerRequest()方法,其处理逻辑随子类的实现不同而不同。

@Component("/login")
public class LoginController implements HttpRequestHandler {
    @Override
    public void handleRequest(HttpServletRequest request, 
    		HttpServletResponse response) 
    		throws ServletException, IOException {
        System.out.println("login...");
        response.getWriter().write("login ...");
    }
}

看上去,HttpRequestHandler接口与Controller接口只差了一个返回值ModelAndView

但是它们的共同点是都有一个路径,而HandlerMapping的作用就是把路径映射到对应的Handler上,其中

用注解@RequestMapping定义的Handler,用的是RequestMappingHandlerMapping,上面的其他两种,用的是BeanNameUrlHandlerMapping,静态资源的请求,用的是SimpleUrlHandlerMapping

http://www.chymfatfish.cn/archives/spring-mvc#gethandler

而又因为这些Handler都不相同,如果想通过DispatcherServlet这个大分发器去统一分发,就有难度了,又不能走接口的形式,因为它们可能入参返回都不一样,这时候使用Adapter适配器就是一个好办法了

例如各个国家电压都不同,但是我的手机只能充220V,这时候就需要一个其他电压转220V的变压器,从而适配各国电压

Adapter就是提供这样的功能,同时适配器比接口更强大的一点在于,它还支持扩展

DispatcherServlet#getHandlerAdapter 中可以看出来,为前面找到的Handler寻找合适的适配器

http://www.chymfatfish.cn/archives/spring-mvc#gethandleradapter

找到之后用适配器调用Handler

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

因为有了适配器,任何形式的Handler都进而可以处理request、response,返回视图

例如上面三种Handler实现中:

  • 实现org.springframework.web.servlet.mvc.Controller接口形式的Handler,对应的HandlerMapping是 BeanNameUrlHandlerMapping,对应的HandlerAdapter 是 HttpRequestHandlerAdapter

  • 实现org.springframework.web.HttpRequestHandler接口形式的Handler,对应的HandlerMapping也是BeanNameUrlHandlerMapping,对应的HandlerAdapter 也是 HttpRequestHandlerAdapter 。

  • 使用@RequestMapping注解的对应的HandlerMapping是RequestMappingHandlerMapping ,对应的HandlerAdapter是RequestMappingHandlerAdapter

其中,@RequestMapping是最没有request、response入参,没有视图返回的形式,可以看下RequestMappingHandlerAdapter#handle方法,里面一定包含了非常多的参数解析和返回处理的相关方法,从而简化开发难度

问题6 - @Transaction注解失效有哪些场景?如何理解?

A:@Transaciton注解失效有以下场景:

  • catch异常不抛出

  • 抛出非RuntimeException或Error类型异常

  • @Transaction类使用private修饰词

  • @Transaction类非外部调用

要理解@Transaction注解失效的原因,要从spring-transaction的原理开始分析

http://www.chymfatfish.cn/archives/spring-transactionshi#%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B

spring-transaction是基于spring-aop通过创建动态代理实现的,即对@Transaction的类创建对应的代理类

由于是java动态代理,很显然自调用是无法应用动态代理的,这是为什么呢?看到动态代理部分:

http://www.chymfatfish.cn/archives/dynamicproxy#jdk%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86

由于在重写的invoke方法中有这样一段:

Object result=  method.invoke(this.target,args);

这里this.target是真实对象,即假如存在这种场景:

@Transaction
public void outIntf() {
    ……
    this.innerIntf();
}

public void innerIntf() {
    ……
}

outIntf通过代理对象调入,代理对象指向method.invoke(this.target,args) 时,调用innerIntf就是真实对象

而自调用的时候,有没有this本质是一样的,都是通过自己的实例对象调用

因此如果把@Transaction加在一个自调用的方法上,能生成代理类,但是没有入口可以调用进来

同理,如果@Transaction是加在private方法上,private方法要么自调用,要么反射调用,都是不可能走代理逻辑的

而关于异常的两个场景,可以看下tracsaction的具体逻辑:

http://www.chymfatfish.cn/archives/spring-transactionshi#completetransactionafterthrowing

可见,代码中判断了异常属于RuntimeException和Error才能处理

而再看下completeTransactionAfterThrowing的调用点:

// org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
       final InvocationCallback invocation) throws Throwable {
    ……
       try {
          retVal = invocation.proceedWithInvocation();
       }
       catch (Throwable ex) {
          // target invocation exception
          completeTransactionAfterThrowing(txInfo, ex);
          throw ex;
       }

可见要能显式catch到异常,才能触发回滚流程

如果自己在代码里面把异常catch并处理了,在代理对象的方法中也无法显式catch到异常了

问题7 - Spring-Junit框架中上下文是如何启动的?

A:基于Junit5的@ExtendWith注解,在加载扩展类时,会自动调用其中的初始化后置处理器方法,而其中就通过本地配置文件启动了spring上下文

Junit和spring-test中的逻辑比较复杂,找对应的逻辑不好找,可以从调用关系(ctrl+alt+h)和debug两个路线一起跟踪

首先回顾搭建基于Spring环境的e2e测试框架案例:

http://www.chymfatfish.cn/archives/e2econstructor

其中有一个SpringTestListner类,获取到了spring上下文

public void beforeTestClass(TestContext testContext) throws Exception {
    ApplicationContext applicationContext = testContext.getApplicationContext();
    this.context = applicationContext;
}

其中调用的是org.springframework.test.context.support.DefaultTestContext#getApplicationContext

public ApplicationContext getApplicationContext() {
    ApplicationContext context = this.cacheAwareContextLoaderDelegate.loadContext(this.mergedContextConfiguration);
    ……
    return context;
}

// org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate#loadContext
public ApplicationContext loadContext(MergedContextConfiguration mergedContextConfiguration) {
	synchronized (this.contextCache) {
		ApplicationContext context = this.contextCache.get(mergedContextConfiguration);
		if (context == null) {
			try {
				context = loadContextInternal(mergedContextConfiguration);
				……
		return context;
	}
}

可以看到这里通过一个缓存机制,如果缓存没有,就调用DefaultCacheAwareContextLoaderDelegate#loadContextInternal

protected ApplicationContext loadContextInternal(MergedContextConfiguration mergedContextConfiguration)
       throws Exception {
   ……
    else {
       String[] locations = mergedContextConfiguration.getLocations();
       Assert.notNull(locations, "Cannot load an ApplicationContext with a NULL 'locations' array. " +
             "Consider annotating your test class with @ContextConfiguration or @ContextHierarchy.");
       return contextLoader.loadContext(locations);
    }
}

这里拿到location,然后启动context,可见与我们调测的XmlApplicationContext逻辑差不多

那么如果把SpringTestListner干掉,是不是就不加载上下文了呢?当然不是了。还有@ExtendWith起效了

我们把断点打在org.springframework.test.context.support.DefaultTestContext#getApplicationContext 中调用loadContext的地方,然后开debug

然后找到@ExtendWith的调用点org.junit.jupiter.engine.descriptor.ExtensionUtils#registerExtensionsFromExecutableParameters

这个调用点继续向上找,看到org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor#prepare

到这里,方法和前面debug重合了

// org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor#prepare
public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) {
    MutableExtensionRegistry registry = populateNewExtensionRegistry(context);
    ThrowableCollector throwableCollector = createThrowableCollector();
    MethodExtensionContext extensionContext = new MethodExtensionContext(context.getExtensionContext(),
       context.getExecutionListener(), this, context.getConfiguration(), throwableCollector);
    throwableCollector.execute(() -> {
       TestInstances testInstances = context.getTestInstancesProvider().getTestInstances(registry,
          throwableCollector);
       extensionContext.setTestInstances(testInstances);
    });

即首先找到被@ExtendWith注释的测试类,封装成registry,然后执行TestInstances testInstances = context.getTestInstancesProvider().getTestInstances(registry, throwableCollector) 这一段代码

跟进

// org.junit.jupiter.engine.execution.TestInstancesProvider#getTestInstances(org.junit.jupiter.engine.extension.MutableExtensionRegistry, org.junit.platform.engine.support.hierarchical.ThrowableCollector)
default TestInstances getTestInstances(MutableExtensionRegistry extensionRegistry,
       ThrowableCollector throwableCollector) {
    return getTestInstances(extensionRegistry, extensionRegistry, throwableCollector);
}

继续跟进

// org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor#instantiateAndPostProcessTestInstance
private TestInstances instantiateAndPostProcessTestInstance(JupiterEngineExecutionContext parentExecutionContext,
       ExtensionContext extensionContext, ExtensionRegistry registry, ExtensionRegistrar registrar,
       ThrowableCollector throwableCollector) {
    TestInstances instances = instantiateTestClass(parentExecutionContext, registry, registrar, extensionContext,
       throwableCollector);
    throwableCollector.execute(() -> {
       invokeTestInstancePostProcessors(instances.getInnermostInstance(), registry, extensionContext);
       // In addition, we register extensions from instance fields here since the
       // best time to do that is immediately following test class instantiation
       // and post processing.
       registerExtensionsFromFields(registrar, this.testClass, instances.getInnermostInstance());
    });
    return instances;
}

看这一段代码,首先instantiateTestClass是初始化测试类,即前面找到的registry,然后调用invokeTestInstancePostProcessors方法调用它的初始化后置处理器

private void invokeTestInstancePostProcessors(Object instance, ExtensionRegistry registry,
       ExtensionContext context) {
    registry.stream(TestInstancePostProcessor.class).forEach(
       extension -> executeAndMaskThrowable(() -> extension.postProcessTestInstance(instance, context)));
}

那么SpringExtension的后置处理器怎么写的呢:

public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception {
    validateAutowiredConfig(context);
    getTestContextManager(context).prepareTestInstance(testInstance);
}

继续跟着debug的调用链,答案呼之欲出

// org.springframework.test.context.support.DependencyInjectionTestExecutionListener#injectDependencies
protected void injectDependencies(TestContext testContext) throws Exception {
	Object bean = testContext.getTestInstance();
	Class<?> clazz = testContext.getTestClass();
	AutowireCapableBeanFactory beanFactory = testContext.getApplicationContext().getAutowireCapableBeanFactory();
	beanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
	beanFactory.initializeBean(bean, clazz.getName() + AutowireCapableBeanFactory.ORIGINAL_INSTANCE_SUFFIX);
	testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);
}

在这里调用testContext.getApplicationContext().getAutowireCapableBeanFactory()完成了applicationContext的加载

问题8 - Condition实现类中能否使用ApplicationContextAware获取bean?

A:不能!因为Condition的实现类加载于ConfigurationClassParser,是ConfigurationClassPostProcessor#processConfigBeanDefinitions 中调用的,即BeanDefinitionRegistryPostProcessor的实现类,它是在AbstractApplicationContext#invokeBeanFactoryPostProcessors 阶段被触发的,而ApplicationContextAware是ApplicationContextAwareProcessor#postProcessBeforeInitialization 调用的,即BeanPostProcessor的实现类,它是在doCreateBean阶段被触发的,远远落后于Condition实现类的触发时机,使用Aware获取bean,只能拿到null报空指针啦

现在有一个需求场景:@Conditional不能使用接口作为条件,因为其底层使用的是BeanUtils.instantiateClass(conditionClass) ,如果使用接口,会抛出接口无法初始化的异常,但是我们需要做一个接口条件,且我们的条件需要由调用方实现,怎么办?

我们重写一个注解类:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(PolicyCondition.class)
public @interface ConditionalOnIntf {
    Class<? extends MyIntf> policy();
}

在写@Configuration的时候,只需要使用我们的注解即可:

@Configuration
public class PolicyService {

    @Bean
    @ConditionalOnSiteIntf(policy = APolicyIntf.class)
    public ServiceIntf getAService() {
        return new AServiceImpl();
    }

    @Bean
    @ConditionalOnSiteIntf(policy = BPolicyIntf.class)
    public ServiceIntf getBService() {
        return new BServiceImpl();
    }
}

即有两种策略,如果匹配上APolicyIntf的结果,就获取到A策略执行器AServiceImpl,反之获取B策略执行器BServiceImpl

而PolicyIntf的实现必须由调用方去重写,即可能与环境有关,例如在flink中执行jar包,还是k8s部署,需要有不同的判断策略

根据注解的传递性,使用@ConditionalOnIntf 注解,即使用@Conditional(PolicyCondition.class)注解

那我们就可以通过一个统一的PolicyCondition.class来处理接口的bean获取

public class PolicyCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        MergedAnnotations annotations = metadata.getAnnotations();
        MergedAnnotation<ConditionalOnSiteIntf> conditionalOnSiteIntfMergedAnnotation = annotations.get(ConditionalOnSiteIntf.class);
        Class value = (Class) conditionalOnSiteIntfMergedAnnotation.getValue("policy").orElse(null);
        if (value == null) {
            return false;
        }
        PolicyIntf policy = (PolicyIntf) context.getBeanFactory().getBean(value);
        return policy.match();
    }
}

在PolicyCondition中,我们重写Condition#match方法,通过从注解中获取到@ConditionalOnSiteIntf中的policy属性,直接基于context中的beanFactory执行getBean操作,获取policy属性接口对应的bean,然后委托给接口bean的match方法,这样可以实现两个效果:

  • 调用方必须自行实现APolicyIntf和BPolicyIntf的实现类并注入成bean

  • APolicy和BPolicy可以通过逻辑,让二者只有一个match,或者都match,条件更加灵活

还是回到最初的问题,如果这里不是要beanFactory,而是使用ApplicaitonContextAware获取bean行吗?显然不行,直接ctrl+alt+h就可以跟踪其调用点,就能很轻易地分析出这个问题了

0

评论区