1. 程式人生 > >Spring IoC @Autowired 註解詳解

Spring IoC @Autowired 註解詳解

# 前言 本系列全部基於 `Spring 5.2.2.BUILD-SNAPSHOT` 版本。因為 Spring 整個體系太過於龐大,所以只會進行關鍵部分的原始碼解析。 我們平時使用 Spring 時,想要 **依賴注入** 時使用最多的是 `@Autowired` 註解了,本文主要講解 Spring 是如何處理該註解並實現 **依賴注入** 的功能。 # 正文 首先我們看一個測試用例: `User` 實體類: ```java public class User { private Long id; private String name; // 省略 get 和 set 方法 } ``` 測試類: ```java public class AnnotationDependencyInjectTest { /** * @Autowired 欄位注入 */ @Autowired private User user; private City city; /** * @Autowired 方法注入 */ @Autowired public void initCity(City city) { this.city = city; } public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(AnnotationDependencyInjectTest.class); context.refresh(); AnnotationDependencyInjectTest bean = context.getBean(AnnotationDependencyInjectTest.class); // @Autowired 欄位注入 System.out.println(bean.user); // @Autowired 方法注入 System.out.println(bean.city); UserHolder userHolder = context.getBean(UserHolder.class); // @Autowired 構造器注入 System.out.println(userHolder.getUser()); context.close(); } @Bean public User user() { User user = new User(); user.setId(1L); user.setName("leisurexi"); return user; } @Bean public City city() { City city = new City(); city.setId(1L); city.setName("北京"); return city; } /** * @Autowired 建構函式注入 */ static class UserHolder { private User user; @Autowired public UserHolder(User user) { this.user = user; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } } } ``` 上面分別展示了 `@Autowired` 註解的欄位注入和方法注入,下面我們開始分析 Spring 是如何實現的。 首先使 `@Autowired` 註解生效的一個關鍵類是 `AutowiredAnnotationBeanPostProcessor`,該類實現了 `InstantiationAwareBeanPostProcessorAdapter` 抽象類;該抽象類就是一個介面卡的作用提供了介面方法的預設實現,`InstantiationAwareBeanPostProcessorAdapter` 又實現了 `SmartInstantiationAwareBeanPostProcessor` 介面,同時實現該介面的 `determineCandidateConstructors()` 方法可以指定 `bean` 的候選建構函式;然後 `SmartInstantiationAwareBeanPostProcessor` 介面又繼承了 `InstantiationAwareBeanPostProcessor` 介面,該介面提供了 `bean` 例項化前後的生命週期回撥以及屬性賦值前的後置處理方法,`@Autowired` 註解的屬性注入就是通過重寫該介面的 `postProcessProperties()` 實現的。這兩個介面都在 [Spring IoC bean 的建立](https://www.cnblogs.com/leisurexi/p/13196998.html) 一文中有介紹過。下面我們看一下 `AutowiredAnnotationBeanProcessor` 的繼承關係圖: ![](http://ww1.sinaimg.cn/large/006Vpl27gy1gfjts4v6uqj30x70dvt9g.jpg) 關於 `AutowiredAnnotationBeanPostProcessor` 這個後置處理器是怎麼加入到 `beanFactory` 中的,我們在 [Spring IoC component-scan 節點詳解](https://www.cnblogs.com/leisurexi/p/13088395.html) 一文中介紹過主要是通過 `AnnotationConfigUtils#registerAnnotationConfigProcessors()` 實現的。 ```java public static Set registerAnnotationConfigProcessors(BeanDefinitionRegistry registry, @Nullable Object source) { // 省略其他程式碼... // 註冊用於處理@Autowired、@Value、@Inject註解的後置處理器 if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) { RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class); def.setSource(source); beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); } // 省略其他程式碼... } ``` ## 屬性和方法注入 `AutowiredAnnotationBeanPostProcessor` 中跟屬性注入有關的方法有兩個:`postProcessMergedBeanDefinition` 和 `postProcessPropertyValues`。 前者是 `MergedBeanDefinitionPostProcessor` 介面中的方法,定義如下: ```java public interface MergedBeanDefinitionPostProcessor extends BeanPostProcessor { /** * 對指定bean的BeanDefinition合併後的處理方法回撥 */ void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName); // 省略其他程式碼... } ``` 後者是 `InstantiationAwareBeanPostProcessor` 介面中的方法,定義如下: ```java public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor { /** * Bean 例項化後屬性賦值前呼叫,PropertyValues 是已經封裝好的設定的屬性值,返回 {@code null} 繼續 * 使用現有屬性,否則會替換 PropertyValues。 */ @Nullable default PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException { return null; } // 省略其他程式碼... } ``` 關於這兩個方法的呼叫時機,可以檢視[Spring IoC bean 的建立](https://www.cnblogs.com/leisurexi/p/13196998.html)和[Spring IoC 屬性賦值階段](https://www.cnblogs.com/leisurexi/p/13234465.html)。 ### bean 合併後處理 #### AutowiredAnnotationBeanPostProcessor#postProcessMergedBeanDefinition 首先執行的是 `postProcessMergedBeanDefinition()` 方法。 ```java public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { // 尋找需要注入的欄位或方法,並封裝成 InjectionMetadata,見下文詳解 InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null); // 檢查元資料中的註解資訊 metadata.checkConfigMembers(beanDefinition); } ``` `InjectionMetadata` 就是注入的元資訊描述,主要欄位如下: ```java public class InjectionMetadata { // 需要依賴注入的目標類 private final Class targetClass; // 注入元素的集合 private final Collection injectedElements; // 忽略其它程式碼 } ``` `InjectedElement` 就是注入的元素,主要欄位如下: ```java public abstract static class InjectedElement { // 注入的屬性或方法 protected final Member member; // 需要注入的是否是欄位 protected final boolean isField; } ``` ### 查詢需要注入的欄位或方法 #### AutowiredAnnotationBeanPostProcessor#findAutowiringMetadata ```java private InjectionMetadata findAutowiringMetadata(String beanName, Class clazz, @Nullable PropertyValues pvs) { String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName()); // 首先從快取中獲取 InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey); // 判斷是否需要重新整理,即metadata為null或者metadata中儲存的targetClass和當前clazz不等 if (InjectionMetadata.needsRefresh(metadata, clazz)) { // 這裡相當於是一個double check,防止多執行緒出現的併發問題 synchronized (this.injectionMetadataCache) { metadata = this.injectionMetadataCache.get(cacheKey); if (InjectionMetadata.needsRefresh(metadata, clazz)) { if (metadata != null) { metadata.clear(pvs); } // 構建注入元資訊,見下文詳解 metadata = buildAutowiringMetadata(clazz); // 放入快取中 this.injectionMetadataCache.put(cacheKey, metadata); } } } // 返回注入元資訊 return metadata; } ``` ### 建立需要注入的元資訊 #### AutowiredAnnotationBeanPostProcessor#buildAutowiringMetadata ```java private InjectionMetadata buildAutowiringMetadata(final Class clazz) { if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) { return InjectionMetadata.EMPTY; } // 判斷當前類或其欄位或其方法是否標註了autowiredAnnotationTypes中的註解,沒有的話直接返回空的 List elements = new ArrayList<>(); Class targetClass = clazz; do { final List currElements = new ArrayList<>(); // 遍歷targetClass中的欄位 ReflectionUtils.doWithLocalFields(targetClass, field -> { // 獲取field上的@Autowired註解資訊 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; } // 獲取@Autowired註解中的required屬性 boolean required = determineRequiredStatus(ann); // 將裝成AutowiredFieldElement新增進currElements currElements.add(new AutowiredFieldElement(field, required)); } }); // 遍歷targetClass中的方法 ReflectionUtils.doWithLocalMethods(targetClass, method -> { // 找到橋接方法 Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); // 判斷方法的可見性,如果不可見則直接返回 if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { return; } // 獲取method上的@Autowired註解資訊 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); } } // 獲取@Autowired註解中的required屬性 boolean required = determineRequiredStatus(ann); // 將方法和目標型別封裝成屬性描述符 PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); // 封裝成AutowiredMethodElement新增進currElements currElements.add(new AutowiredMethodElement(method, required, pd)); } }); // 將currElements整個新增進elements elements.addAll(0, currElements); // 獲取targetClass的父類,進行下一次迴圈 targetClass = targetClass.getSuperclass(); } // 當targetClass為空或者targetClass等於Object.class時會退出迴圈 while (targetClass != null && targetClass != Object.class); // 將elements和clazz封裝成InjectionMetadata返回 return InjectionMetadata.forElements(elements, clazz); } ``` 上面程式碼中的 `findAutowiredAnnotation()` 就是在遍歷 `autowiredAnnotationTypes` 屬性,看欄位或者方法上的註解是否存在於 `autowiredAnnotationTypes` 中,或者其派生註解,找到第一個就返回,不會再繼續遍歷了。 ```java public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware { private final Set> autowiredAnnotationTypes = new LinkedHashSet<>(4); public AutowiredAnnotationBeanPostProcessor() { this.autowiredAnnotationTypes.add(Autowired.class); this.autowiredAnnotationTypes.add(Value.class); try { this.autowiredAnnotationTypes.add((Class) ClassUtils.forName("javax.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader())); logger.trace("JSR-330 'javax.inject.Inject' annotation found and supported for autowiring"); } catch (ClassNotFoundException ex) { // JSR-330 API not available - simply skip. } } } ``` 從 `AutowiredAnnotationBeanPostProcessor` 類的建構函式中,我們可以發現 `autowiredAnnotationTypes` 預設添加了 `@Autowired`、`@Value` 以及 `@Inject` (在 JSR-330 的jar包存在於當前環境時)。 至此,使用 `@Autowired` 修飾的欄位和方法已經封裝成 `InjectionMetadata` 並放在 `injectionMetadataCache` 快取中,便於後續使用。 ### bean 屬性的後置處理 #### AutowireAnnotationBeanPostProcessor#postProcessProperties `postProcessMergedBeanDefinition()` 呼叫後 `bean` 就會進行例項化接著呼叫 `postProcessProperties()` 。 ```java public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { // 獲取快取中的 InjectionMetadata InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs); try { // 進行屬性的注入 metadata.inject(bean, beanName, pvs); } catch (BeanCreationException ex) { throw ex; } catch (Throwable ex) { throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex); } // 返回注入的屬性 return pvs; } // InjectMetadata.java public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { // 獲取檢查後的元素 Collection checkedElements = this.checkedElements; // 如果checkedElements不為空就使用checkedElements,否則使用injectedElements Collection elementsToIterate = (checkedElements != null ? checkedElements : this.injectedElements); if (!elementsToIterate.isEmpty()) { // 遍歷elementsToIterate for (InjectedElement element : elementsToIterate) { if (logger.isTraceEnabled()) { logger.trace("Processing injected element of bean '" + beanName + "': " + element); } // AutowiredFieldElement、AutowiredMethodElement這兩個類繼InjectionMetadata.InjectedElement // 各自重寫了inject方法 element.inject(target, beanName, pvs); } } } ``` ### 欄位注入 #### AutowiredFieldElement#inject ```java protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { // 強轉成Field型別 Field field = (Field) this.member; Object value; if (this.cached) { // 如果快取過,直接使用快取的值,一般第一次注入都是false value = resolvedCachedArgument(beanName, this.cachedFieldValue); } else { // 構建依賴描述符 DependencyDescriptor desc = new DependencyDescriptor(field, this.required); desc.setContainingClass(bean.getClass()); Set autowiredBeanNames = new LinkedHashSet<>(1); Assert.state(beanFactory != null, "No BeanFactory available"); // 獲取型別轉換器 TypeConverter typeConverter = beanFactory.getTypeConverter(); try { // 進行依賴解決,獲取符合條件的bean value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter); } catch (BeansException ex) { throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex); } // 加鎖 synchronized (this) { // 如果沒有被快取 if (!this.cached) { // 找到了需要的bean || 該欄位是必要的 if (value != null || this.required) { // 將依賴描述符賦值給cachedFieldValue this.cachedFieldValue = desc; // 註冊bean的依賴關係,用於檢測是否迴圈依賴 registerDependentBeans(beanName, autowiredBeanNames); // 如果符合條件的bean只有一個 if (autowiredBeanNames.size() == 1) { String autowiredBeanName = autowiredBeanNames.iterator().next(); // beanFactory含有名為autowiredBeanName的bean && 型別是匹配的 if (beanFactory.containsBean(autowiredBeanName) && beanFactory.isTypeMatch(autowiredBeanName, field.getType())) { // 將該屬性解析到的bean的資訊封裝成ShortcutDependencyDescriptor // 之後可以通過呼叫resolveShortcut()來間接調beanFactory.getBean()快速獲取bean this.cachedFieldValue = new ShortcutDependencyDescriptor( desc, autowiredBeanName, field.getType()); } } } else { this.cachedFieldValue = null; } // 快取標識設定為true this.cached = true; } } } // 如果找到了符合的bean,設定欄位可訪問,利用反射設定值 if (value != null) { ReflectionUtils.makeAccessible(field); field.set(bean, value); } } ``` 上面程式碼中的 `beanFactory.resolveDependency()` 在 [Spring IoC bean 的建立](https://www.cnblogs.com/leisurexi/p/13196998.html) 一文中有介紹過,這裡不再贅述;同樣 `registerDependentBeans()` 最終會呼叫 `DefaultSingletonBeanRegistry.registerDependentBean()` ,該方法在 [Spring IoC bean 的建立](https://www.cnblogs.com/leisurexi/p/13196998.html) 一文中有介紹過,這裡也不再贅述。 ### 方法注入 #### AutowiredMethodElement#inject ```java protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { // 檢查是否需要跳過 if (checkPropertySkipping(pvs)) { return; } // 強轉成Method型別 Method method = (Method) this.member; Object[] arguments; if (this.cached) { // 如果快取過,直接呼叫beanFactory.resolveDependency()返回符合的bean arguments = resolveCachedArguments(beanName); } else { // 獲取引數數量 int argumentCount = method.getParameterCount(); arguments = new Object[argumentCount]; // 建立依賴描述符陣列 DependencyDescriptor[] descriptors = new DependencyDescriptor[argumentCount]; // 記錄用於自動注入bean的名稱集合 Set autowiredBeans = new LinkedHashSet<>(argumentCount); Assert.state(beanFactory != null, "No BeanFactory available"); // 獲取型別轉換器 TypeConverter typeConverter = beanFactory.getTypeConverter(); // 遍歷引數 for (int i = 0; i < arguments.length; i++) { // 將方法和引數的下標構建成MethodParameter,這裡面主要記錄了引數的下標和型別 MethodParameter methodParam = new MethodParameter(method, i); // 將MethodParameter構建成DependencyDescriptor DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required); currDesc.setContainingClass(bean.getClass()); descriptors[i] = currDesc; try { // 進行依賴解決,找到符合條件的bean Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter); if (arg == null && !this.required) { arguments = null; break; } arguments[i] = arg; } catch (BeansException ex) { throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex); } } // 這裡跟欄位注入差不多,就是註冊bean的依賴關係,並且快取每個引數的ShortcutDependencyDescriptor synchronized (this) { if (!this.cached) { if (arguments != null) { DependencyDescriptor[] cachedMethodArguments = Arrays.copyOf(descriptors, arguments.length); registerDependentBeans(beanName, autowiredBeans); if (autowiredBeans.size() == argumentCount) { Iterator it = autowiredBeans.iterator(); Class[] paramTypes = method.getParameterTypes(); for (int i = 0; i < paramTypes.length; i++) { String autowiredBeanName = it.next(); if (beanFactory.containsBean(autowiredBeanName) && beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) { cachedMethodArguments[i] = new ShortcutDependencyDescriptor(descriptors[i], autowiredBeanName, paramTypes[i]); } } } this.cachedMethodArguments = cachedMethodArguments; } else { this.cachedMethodArguments = null; } this.cached = true; } } } // 找到了符合條件的bean if (arguments != null) { try { // 設定方法可訪問,利用反射進行方法呼叫,傳入引數 ReflectionUtils.makeAccessible(method); method.invoke(bean, arguments); } catch (InvocationTargetException ex) { throw ex.getTargetException(); } } } ``` ## 構造器注入 構造器注入就是通過呼叫 `determineCandidateConstructors()` 來返回合適的構造器。 ```java public Constructor[] determineCandidateConstructors(Class beanClass, final String beanName) throws BeanCreationException { // Quick check on the concurrent map first, with minimal locking. // 首先從快取中獲取 Constructor[] candidateConstructors = this.candidateConstructorsCache.get(beanClass); // 快取為空 if (candidateConstructors == null) { // Fully synchronized resolution now... // 這裡相當於double check synchronized (this.candidateConstructorsCache) { candidateConstructors = this.candidateConstructorsCache.get(beanClass); if (candidateConstructors == null) { Constructor[] rawCandidates; try { // 獲取beanClass的所有建構函式 rawCandidates = beanClass.getDeclaredConstructors(); } catch (Throwable ex) { throw new BeanCreationException(beanName, "Resolution of declared constructors on bean Class [" + beanClass.getName() +"] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex); } // 存放標註了@Autowired註解的構造器 List> candidates = new ArrayList<>(rawCandidates.length); // 存放標註了@Autowired註解,並且required為true的構造器 Constructor requiredConstructor = null; Constructor defaultConstructor = null; for (Constructor candidate : rawCandidates) { // 獲取構造器上的@Autowired註解資訊 MergedAnnotation ann = findAutowiredAnnotation(candidate); if (ann == null) { // 如果沒有從候選者找到註解,則嘗試解析beanClass的原始類(針對CGLIB代理) Class userClass = ClassUtils.getUserClass(beanClass); if (userClass != beanClass) { try { Constructor superCtor = userClass.getDeclaredConstructor(candidate.getParameterTypes()); ann = findAutowiredAnnotation(superCtor); } catch (NoSuchMethodException ex) { // Simply proceed, no equivalent superclass constructor found... } } } if (ann != null) { // 如果requiredConstructor不為空,代表有多個標註了@Autowired且required為true的構造器,此時Spring不知道選擇哪個丟擲異常 if (requiredConstructor != null) { throw new BeanCreationException(beanName, "Invalid autowire-marked constructor: " + candidate +". Found constructor with 'required' Autowired annotation already: " + requiredConstructor); } // 獲取@Autowired註解的reuired屬性的值 boolean required = determineRequiredStatus(ann); if (required) { // 如果當前候選者是@Autowired(required = true),則之前不能存在其他使用@Autowire註解的建構函式,否則拋異常 if (!candidates.isEmpty()) { throw new BeanCreationException(beanName,"Invalid autowire-marked constructors: " + candidates +". Found constructor with 'required' Autowired annotation: " + candidate); } // required為true將當前構造器賦值給requiredConstructor requiredConstructor = candidate; } // 將當前構造器加入進候選構造器中 candidates.add(candidate); } // 沒有標註了@Autowired註解且引數長度為0,賦值為預設構造器 else if (candidate.getParameterCount() == 0) { defaultConstructor = candidate; } } // 有標註了@Autowired註解的構造器 if (!candidates.isEmpty()) { // Add default constructor to list of optional constructors, as fallback. // 沒有標註了@Autowired且required為true的構造器 if (requiredConstructor == null) { // 預設構造器不為空 if (defaultConstructor != null) { // 將預設構造器加入進候選構造器中 candidates.add(defaultConstructor); } } // 將候選者賦值給candidateConstructors candidateConstructors = candidates.toArray(new Constructor[0]); } // 只有1個構造器 && 引數長度大於0(非預設構造器),只能用它當做候選者了 else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) { candidateConstructors = new Constructor[] {rawCandidates[0]}; } // 只有1個構造器 && 引數長度大於0,只能用它當做候選者了 else if (nonSyntheticConstructors == 2 && primaryConstructor != null && defaultConstructor != null && !primaryConstructor.equals(defaultConstructor)) { candidateConstructors = new Constructor[] {primaryConstructor, defaultConstructor}; } else if (nonSyntheticConstructors == 1 && primaryConstructor != null) { candidateConstructors = new Constructor[] {primaryConstructor}; } // 返回一個空的Constructor else { candidateConstructors = new Constructor[0]; } // 快取候選的構造器 this.candidateConstructorsCache.put(beanClass, candidateConstructors); } } } // 如果候選構造器長度大於0,直接返回,否則返回null return (candidateConstructors.length > 0 ? candidateConstructors : null); } ``` 關於 `SmartInstantiationAwareBeanPostProcessor` 介面的呼叫時機,在 [Spring IoC bean 的建立](https://www.cnblogs.com/leisurexi/p/13196998.html) 一文中有介紹過,這裡就不再贅述了。 # 總結 本文主要介紹了 Spring 對 `@Autowired` 註解的主要處理過程,結合前面的 [Spring IoC bean 的載入](https://www.cnblogs.com/leisurexi/p/13194515.html) 和 [Spring IoC bean 的建立](https://www.cnblogs.com/leisurexi/p/13196998.html) 以及 [Spring IoC 屬性賦值階段](https://www.cnblogs.com/leisurexi/p/13234465.html) 一起看才能更好的理解。 > 最後,我模仿 Spring 寫了一個精簡版,程式碼會持續更新。地址:[https://github.com/leisurexi/tiny-spring](https://github.com/leisurexi/tiny-spr