问题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创建流程看:
问题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这边已经看过了
这次针对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部分的源码
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
而又因为这些Handler都不相同,如果想通过DispatcherServlet这个大分发器去统一分发,就有难度了,又不能走接口的形式,因为它们可能入参返回都不一样,这时候使用Adapter适配器就是一个好办法了
例如各个国家电压都不同,但是我的手机只能充220V,这时候就需要一个其他电压转220V的变压器,从而适配各国电压
Adapter就是提供这样的功能,同时适配器比接口更强大的一点在于,它还支持扩展
在DispatcherServlet#getHandlerAdapter
中可以看出来,为前面找到的Handler寻找合适的适配器
找到之后用适配器调用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的原理开始分析
spring-transaction是基于spring-aop通过创建动态代理实现的,即对@Transaction的类创建对应的代理类
由于是java动态代理,很显然自调用是无法应用动态代理的,这是为什么呢?看到动态代理部分:
由于在重写的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的具体逻辑:
可见,代码中判断了异常属于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测试框架案例:
其中有一个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就可以跟踪其调用点,就能很轻易地分析出这个问题了
评论区