跳到主要內容

spring源碼解析之IOC容器(二)------加載和註冊

  上一篇跟蹤了IOC容器對配置文件的定位,現在我們繼續跟蹤代碼,看看IOC容器是怎麼加載和註冊配置文件中的信息的。開始之前,首先我們先來了解一下IOC容器所使用的數據結構-------BeanDefinition,它是一個上層接口,有很多實現類,分別對應不同的數據載體。我們平時開發的時候,也會定義很多pojo類,來作為獲取數據的載體。最常見的就是,從數據庫中獲取數據之後,使用一個定義的pojo來裝載,然後我們就可以在程序中使用這個pojo類來編寫各種業務邏輯。同樣,IOC容器首先會讀取配置的XML中各個節點,即各個標籤元素,然後根據不同的標籤元素,使用不同的數據結構來裝載該元素中的各種屬性的值。比如我們最熟悉的<bean>標籤,就是使用AbstractBeanDefinition這個數據結構,接下來的分析中我們可以看到。


  先回到上篇資源的定位那裡,代碼如下:


 1 public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException { 
2 ResourceLoader resourceLoader = getResourceLoader();
3 if (resourceLoader == null) {
4 throw new BeanDefinitionStoreException(
5 "Cannot import bean definitions from location [" + location + "]: no ResourceLoader available");
6 }
7
8 if (resourceLoader instanceof ResourcePatternResolver) {
9 // Resource pattern matching available.
10 try {
11 Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
12 int loadCount = loadBeanDefinitions(resources);
13 if (actualResources != null) {
14 for (Resource resource : resources) {
15 actualResources.add(resource);
16 }
17 }
18 if (logger.isDebugEnabled()) {
19 logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]");
20 }
21 return loadCount;
22 }
23 catch (IOException ex) {
24 throw new BeanDefinitionStoreException(
25 "Could not resolve bean definition resource pattern [" + location + "]", ex);
26 }
27 }
28 else {
29 // 定位到資源之後,封裝成一個resource對象
30 Resource resource = resourceLoader.getResource(location);
31 int loadCount = loadBeanDefinitions(resource);
32 if (actualResources != null) {
33 actualResources.add(resource);
34 }
35 if (logger.isDebugEnabled()) {
36 logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]");
37 }
38 return loadCount;
39 }
40 }

  進入loadBeanDefinitions(resource)方法,正式開始加載源碼的跟蹤:


1         @Override 
2 public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
3 return loadBeanDefinitions(new EncodedResource(resource));
4 }

 1 public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { 
2 Assert.notNull(encodedResource, "EncodedResource must not be null");
3 if (logger.isInfoEnabled()) {
4 logger.info("Loading XML bean definitions from " + encodedResource);
5 }
6
7 Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
8 if (currentResources == null) {
9 currentResources = new HashSet<EncodedResource>(4);
10 this.resourcesCurrentlyBeingLoaded.set(currentResources);
11 }
12 if (!currentResources.add(encodedResource)) {
13 throw new BeanDefinitionStoreException(
14 "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
15 }
16 try {
17 InputStream inputStream = encodedResource.getResource().getInputStream();
18 try {
19 InputSource inputSource = new InputSource(inputStream);
20 if (encodedResource.getEncoding() != null) {
21 inputSource.setEncoding(encodedResource.getEncoding());
22 }
23 return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
24 }
25 finally {
26 inputStream.close();
27 }
28 }
29 catch (IOException ex) {
30 throw new BeanDefinitionStoreException(
31 "IOException parsing XML document from " + encodedResource.getResource(), ex);
32 }
33 finally {
34 currentResources.remove(encodedResource);
35 if (currentResources.isEmpty()) {
36 this.resourcesCurrentlyBeingLoaded.remove();
37 }
38 }
39 }

  進入doLoadBeanDefinitions(inputSource, encodedResource.getResource())方法:


 1 protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) 
2 throws BeanDefinitionStoreException {
3 try {
4 Document doc = doLoadDocument(inputSource, resource);
5 return registerBeanDefinitions(doc, resource);
6 }
7 catch (BeanDefinitionStoreException ex) {
8 throw ex;
9 }
10 catch (SAXParseException ex) {
11 throw new XmlBeanDefinitionStoreException(resource.getDescription(),
12 "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
13 }
14 catch (SAXException ex) {
15 throw new XmlBeanDefinitionStoreException(resource.getDescription(),
16 "XML document from " + resource + " is invalid", ex);
17 }
18 catch (ParserConfigurationException ex) {
19 throw new BeanDefinitionStoreException(resource.getDescription(),
20 "Parser configuration exception parsing XML from " + resource, ex);
21 }
22 catch (IOException ex) {
23 throw new BeanDefinitionStoreException(resource.getDescription(),
24 "IOException parsing XML document from " + resource, ex);
25 }
26 catch (Throwable ex) {
27 throw new BeanDefinitionStoreException(resource.getDescription(),
28 "Unexpected exception parsing XML document from " + resource, ex);
29 }
30 }

  繼續進入registerBeanDefinitions(doc, resource)方法:


1 public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { 
2 //此時documentReader已經是DefaultBeanDefinitionDocumentReader類了
3 BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
4 int countBefore = getRegistry().getBeanDefinitionCount();
5 documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
6 //返回當前註冊的beanDefinition的個數
7 return getRegistry().getBeanDefinitionCount() - countBefore;
8 }

  進入registerBeanDefinitions(doc, createReaderContext(resource))方法:


1 public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { 
2 this.readerContext = readerContext;
3 logger.debug("Loading bean definitions");
4 Element root = doc.getDocumentElement();
5 doRegisterBeanDefinitions(root);
6 }

  進入doRegisterBeanDefinitions(root)方法:


 1 protected void doRegisterBeanDefinitions(Element root) { 
2 // Any nested <beans> elements will cause recursion in this method. In
3 // order to propagate and preserve <beans> default-* attributes correctly,
4 // keep track of the current (parent) delegate, which may be null. Create
5 // the new (child) delegate with a reference to the parent for fallback purposes,
6 // then ultimately reset this.delegate back to its original (parent) reference.
7 // this behavior emulates a stack of delegates without actually necessitating one.
8 BeanDefinitionParserDelegate parent = this.delegate;
9 this.delegate = createDelegate(getReaderContext(), root, parent);
10
11 if (this.delegate.isDefaultNamespace(root)) {
12 //profile屬性平時使用非常少,該屬性可以用於配置數據庫的切換(常用),使用時,需要在web.xml中配置context-parm
13 //<context-parm>
14 // <parm-name>Spring.profiles.active</parm-name>
15 // <parm-value>dev(在applicationContext.xml中配置的profile屬性的beans的profile屬性值)</parm-name>
16 //</context-parm>
17 //在applicationContext.xml中的配置
18 //<beans profile="dev"> </beans>
19 //<beans profile="produce"> </beans>
20 String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
21 if (StringUtils.hasText(profileSpec)) {
22 String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
23 profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
24 if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
25 if (logger.isInfoEnabled()) {
26 logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
27 "] not matching: " + getReaderContext().getResource());
28 }
29 return;
30 }
31 }
32 }
33
34 preProcessXml(root);
35 parseBeanDefinitions(root, this.delegate);
36 postProcessXml(root);
37
38 this.delegate = parent;
39 }

  這裏也用到了模板方法,preProcessXml(root)和postProcessXml(root)這兩個方法都是空實現,是留給客戶來實現自己的邏輯的。重點研究一下parseBeanDefinitions(root, this.delegate)方法:


 1 protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { 
2 if (delegate.isDefaultNamespace(root)) {
3 NodeList nl = root.getChildNodes();
4 for (int i = 0; i < nl.getLength(); i++) {
5 Node node = nl.item(i);
6 if (node instanceof Element) {
7 Element ele = (Element) node;
8 if (delegate.isDefaultNamespace(ele)) {
9 parseDefaultElement(ele, delegate);
10 }
11 else {
12 delegate.parseCustomElement(ele);
13 }
14 }
15 }
16 }
17 else {
18 delegate.parseCustomElement(root);
19 }
20 }

  parseCustomElement(root)方法不需要怎麼研究,我們平時幾乎不會用到自定義的標籤,所以只跟蹤parseDefaultElement(ele, delegate)裏面的代碼:


private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { 
//import標籤
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
//alias標籤
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
//bean標籤
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
}
//beans標籤
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
doRegisterBeanDefinitions(ele);
}
}

  可以看到,對於不同的標籤,spring採用不同的策略進行處理,重點跟蹤一下處理bean標籤的方法processBeanDefinition(ele, delegate):


 1 protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { 
2 //委託給delegate去進行各種標籤的解析,parseBeanDefinitionElement方法中包含了各種標籤元素的解析,
3 //並將解析好的內容封裝成BeanDefinitionHolder對象
4 BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
5 if (bdHolder != null) {
6 bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
7 try {
8 // Register the final decorated instance.
9 BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
10 }
11 catch (BeanDefinitionStoreException ex) {
12 getReaderContext().error("Failed to register bean definition with name '" +
13 bdHolder.getBeanName() + "'", ele, ex);
14 }
15 // Send registration event.
16 getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
17 }
18 }

  在這個方法中,delegate.parseBeanDefinitionElement(ele)是解析bean元素中各種屬性的方法,registerBeanDefinition(bdHolder, getReaderContext().getRegistry())是將封裝好的數據進行存儲的方法。先看一下解析的方法:


 1 public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, BeanDefinition containingBean) { 
2 //獲取bean標籤的id屬性的值
3 String id = ele.getAttribute(ID_ATTRIBUTE);
4 //獲取bean標籤上name屬性的值
5 String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
6
7 List<String> aliases = new ArrayList<String>();
8 if (StringUtils.hasLength(nameAttr)) {
9 //將name的值進行分割,並將它們當作別名存到aliases中
10 String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
11 aliases.addAll(Arrays.asList(nameArr));
12 }
13
14 String beanName = id;
15 //如果bean標籤的id沒有值,但是name屬性有值,則將name屬性的第一個值當作id的值,並從aliases中將第一個別名移除掉
16 if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
17 beanName = aliases.remove(0);
18 if (logger.isDebugEnabled()) {
19 logger.debug("No XML 'id' specified - using '" + beanName +
20 "' as bean name and " + aliases + " as aliases");
21 }
22 }
23
24 if (containingBean == null) {
25 //檢查bean的唯一性
26 checkNameUniqueness(beanName, aliases, ele);
27 }
28
29 //這裏已經是將XML中bean元素中的所有屬性都封裝到beanDefinition對象中了
30 AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
31 if (beanDefinition != null) {
32 if (!StringUtils.hasText(beanName)) {
33 try {
34 if (containingBean != null) {
35 beanName = BeanDefinitionReaderUtils.generateBeanName(
36 beanDefinition, this.readerContext.getRegistry(), true);
37 }
38 else {
39 beanName = this.readerContext.generateBeanName(beanDefinition);
40 // Register an alias for the plain bean class name, if still possible,
41 // if the generator returned the class name plus a suffix.
42 // This is expected for Spring 1.2/2.0 backwards compatibility.
43 String beanClassName = beanDefinition.getBeanClassName();
44 if (beanClassName != null &&
45 beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
46 !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
47 aliases.add(beanClassName);
48 }
49 }
50 if (logger.isDebugEnabled()) {
51 logger.debug("Neither XML 'id' nor 'name' specified - " +
52 "using generated bean name [" + beanName + "]");
53 }
54 }
55 catch (Exception ex) {
56 error(ex.getMessage(), ele);
57 return null;
58 }
59 }
60 String[] aliasesArray = StringUtils.toStringArray(aliases);
61 //最後將封裝好的beanDefinition、它的id、以及它的別名一起封裝成BeanDefinitionHolder對象返回
62 return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
63 }
64
65 return null;
66 }

  我們可以得到如下信息:


  1、獲取bean標籤的id屬性和name屬性的值;


  2、name屬性是可以都有多個值的,以逗號或者分號分割;


  3、如果id沒有賦值,則取name的第一個值作為id的值。所以,我們一般都會給id賦值,這樣效率高一些;


  4、檢查以這個id標識的bean是不是唯一的;


  5、進行其他屬性的解析,並最終封裝測AbstractBeanDefinition對象,也就是我們前文中提到的數據結構;


  6、最後封裝成BeanDefinitionHolder對象之後返回。


  進入parseBeanDefinitionElement(ele, beanName, containingBean)方法,看一下其他元素的解析過程:


 1 public AbstractBeanDefinition parseBeanDefinitionElement( 
2 Element ele, String beanName, BeanDefinition containingBean) {
3
4 this.parseState.push(new BeanEntry(beanName));
5
6 String className = null;
7 if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
8 className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
9 }
10
11 try {
12 String parent = null;
13 if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
14 parent = ele.getAttribute(PARENT_ATTRIBUTE);
15 }
16 AbstractBeanDefinition bd = createBeanDefinition(className, parent);
17
18 parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
19 bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
20
21 parseMetaElements(ele, bd);
22 parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
23 parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
24
25 parseConstructorArgElements(ele, bd);
26 parsePropertyElements(ele, bd);
27 parseQualifierElements(ele, bd);
28
29 bd.setResource(this.readerContext.getResource());
30 bd.setSource(extractSource(ele));
31
32 return bd;
33 }
34 catch (ClassNotFoundException ex) {
35 error("Bean class [" + className + "] not found", ele, ex);
36 }
37 catch (NoClassDefFoundError err) {
38 error("Class that bean class [" + className + "] depends on not found", ele, err);
39 }
40 catch (Throwable ex) {
41 error("Unexpected failure during bean definition parsing", ele, ex);
42 }
43 finally {
44 this.parseState.pop();
45 }
46
47 return null;
48 }

  解析封裝成BeanDefinitionHolder對象之後,就可以進行註冊了,先回到之前的processBeanDefinition(ele, delegate):


 1 protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { 
2 //委託給delegate去進行各種標籤的解析,parseBeanDefinitionElement方法中包含了各種標籤元素的解析,
3 //並將解析好的內容封裝成BeanDefinitionHolder對象
4 BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
5 if (bdHolder != null) {
6 bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
7 try {
8 // Register the final decorated instance.
9 BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
10 }
11 catch (BeanDefinitionStoreException ex) {
12 getReaderContext().error("Failed to register bean definition with name '" +
13 bdHolder.getBeanName() + "'", ele, ex);
14 }
15 // Send registration event.
16 getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
17 }
18 }

  現在進入BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry())方法進行分析:


 1 public static void registerBeanDefinition( 
2 BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
3 throws BeanDefinitionStoreException {
4
5 // Register bean definition under primary name.
6 String beanName = definitionHolder.getBeanName();
7 registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
8
9 // Register aliases for bean name, if any.
10 String[] aliases = definitionHolder.getAliases();
11 if (aliases != null) {
12 for (String alias : aliases) {
13 registry.registerAlias(beanName, alias);
14 }
15 }
16 }

  這裏的beanName就是之前封裝好的bean的id。這個方法中分別以id和別名作為key來註冊bean,其實就是存儲在map中。


  進入registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()),在其子類DefaultListableBeanFactory中有實現:


 1 public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) 
2 throws BeanDefinitionStoreException {
3
4 Assert.hasText(beanName, "Bean name must not be empty");
5 Assert.notNull(beanDefinition, "BeanDefinition must not be null");
6
7 if (beanDefinition instanceof AbstractBeanDefinition) {
8 try {
9 ((AbstractBeanDefinition) beanDefinition).validate();
10 }
11 catch (BeanDefinitionValidationException ex) {
12 throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
13 "Validation of bean definition failed", ex);
14 }
15 }
16
17 BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
18 if (existingDefinition != null) {
19 if (!isAllowBeanDefinitionOverriding()) {
20 throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
21 "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
22 "': There is already [" + existingDefinition + "] bound.");
23 }
24 else if (existingDefinition.getRole() < beanDefinition.getRole()) {
25 // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
26 if (logger.isWarnEnabled()) {
27 logger.warn("Overriding user-defined bean definition for bean '" + beanName +
28 "' with a framework-generated bean definition: replacing [" +
29 existingDefinition + "] with [" + beanDefinition + "]");
30 }
31 }
32 else if (!beanDefinition.equals(existingDefinition)) {
33 if (logger.isInfoEnabled()) {
34 logger.info("Overriding bean definition for bean '" + beanName +
35 "' with a different definition: replacing [" + existingDefinition +
36 "] with [" + beanDefinition + "]");
37 }
38 }
39 else {
40 if (logger.isDebugEnabled()) {
41 logger.debug("Overriding bean definition for bean '" + beanName +
42 "' with an equivalent definition: replacing [" + existingDefinition +
43 "] with [" + beanDefinition + "]");
44 }
45 }
46 this.beanDefinitionMap.put(beanName, beanDefinition);
47 }
48 else {
49 if (hasBeanCreationStarted()) {
50 // Cannot modify startup-time collection elements anymore (for stable iteration)
51 synchronized (this.beanDefinitionMap) {
52 this.beanDefinitionMap.put(beanName, beanDefinition);
53 List<String> updatedDefinitions = new ArrayList<String>(this.beanDefinitionNames.size() + 1);
54 updatedDefinitions.addAll(this.beanDefinitionNames);
55 updatedDefinitions.add(beanName);
56 this.beanDefinitionNames = updatedDefinitions;
57 if (this.manualSingletonNames.contains(beanName)) {
58 Set<String> updatedSingletons = new LinkedHashSet<String>(this.manualSingletonNames);
59 updatedSingletons.remove(beanName);
60 this.manualSingletonNames = updatedSingletons;
61 }
62 }
63 }
64 else {
65 // Still in startup registration phase
66 this.beanDefinitionMap.put(beanName, beanDefinition);
67 this.beanDefinitionNames.add(beanName);
68 this.manualSingletonNames.remove(beanName);
69 }
70 this.frozenBeanDefinitionNames = null;
71 }
72
73 if (existingDefinition != null || containsSingleton(beanName)) {
74 resetBeanDefinition(beanName);
75 }
76 }

  我們可以看到:這個beanDefinitionMap就是用來存儲解析好的bean的,以id作為key。至此,就將所有的bean標籤解析好之後封裝成BeanDefinition註冊到了IOC容器中。但是,到目前為止,IOC容器並沒有為我們將這些解析好的數據生成一個一個bean實例,我們仍然不能就這樣直接使用。下一篇接着跟蹤。

【精選推薦文章】



自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象



網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!



評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享



台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"



Orignal From: spring源碼解析之IOC容器(二)------加載和註冊

留言

這個網誌中的熱門文章

Python 併發總結,多線程,多進程,異步IO

1 測量函數運行時間 import time def profile(func): def wrapper(*args, ** kwargs): import time start = time.time() func( *args, ** kwargs) end = time.time() print ' COST: {} ' .format(end - start) return wrapper @profile def fib(n): if n<= 2 : return 1 return fib(n-1) + fib(n-2 ) fib( 35 )   2 啟動多個線程,並等待完成   2.1 使用threading.enumerate() import threading for i in range(2 ): t = threading.Thread(target=fib, args=(35 ,)) t.start() main_thread = threading.currentThread() for t in threading.enumerate(): if t is main_thread: continue t.join()   2.2 先保存啟動的線程 threads = [] for i in range(5 ): t = Thread(target=foo, args= (i,)) threads.append(t) t.start() for t in threads: t.join()   3 使用信號量,限制同時能有幾個線程訪問臨界區 from threading import Semaphore import time sema = Semaphor...

高雄十大包子名店出爐

, 圖文:吳恩文 高雄包子大賽落幕了,我只能就我個人意見, 介紹一下前十名這些包子,但是不能代表其他四位評審的意見,雖然身為評審長,我通常不會第一個表示意見,以免影響其他評審, 我主要工作是負責發問。   這次參賽的素包子很少,而且都不夠細致,又偏油,我不愛, 但是第一名的甜芝麻包-熔岩黑金包,竟然是素食得名- 漢來蔬食巨蛋店。   這包子賣相太好,竹炭粉的黑色外皮刷上金粉,一上桌,眾人驚呼, 搶拍照,內餡是芝麻餡,混一點花生醬增稠,加入白糖芝麻油, 熔岩爆漿的程度剛剛好,我一直以為芝麻要配豬油才行、 但是選到好的黑芝麻油一樣不減香醇, 當下有二位評審就想宅配回家。   尤其特別的是,黑芝麻餡室溫易化,師傅必須要輪班躲在冷藏室內, 穿著大外套才能包,一天包不了多少,我笑說,漢來美食,集團餐廳那麼多,實力雄厚,根本是「 奧運選手報名參加村裡運動會」嘛,其他都是小包子店啊, 但是沒辦法,顯然大家都覺得它好看又好吃, 目前限定漢來蔬食高雄巨蛋店,二顆88元,可以冷凍宅配, 但是要排一陣子,因為供不應求,聽說,四月份, 台北sogo店開始會賣。   第二名的包子,左營寬來順早餐店,顯然平易近人的多,一顆肉包, 十塊錢,是所有參賽者中最便宜的,當然,個頭也小, 它的包子皮明顯和其他不同,灰灰的老麵,薄但紮實有嚼勁, 肉餡新鮮帶汁,因為打了些水,味道極其簡單,就是蔥薑,塩, 香油,薑味尤其明顯,是老眷村的味道, 而特別的是老闆娘是台灣本省人, 當年完全是依據眷村老兵的口味一步一步調整而來,沒有加什麼糖、 五香粉,胡椒粉,油蔥酥。就是蔥薑豬肉和老麵香,能得名, 應該是它的平實無華,鮮美簡單,打動人心。   這是標準的心靈美食,可以撫慰人心,得名之前,寛來順已經天天排隊,現在,恐怕要排更久了, 建議大家六七點早點上門。   第三名,「專十一」很神奇,我記得比賽最後, 大家連吃了幾家不能引起共鳴的包子,有些累,到了專十一, 就坐著等包子,其他評審一吃,就催我趕快試,我一吃, 也醒了大半。   它的包子皮厚薄適中,但是高筋麵粉高些,老麵加一點點酵母, 我心中,它的皮屬一屬二,至於餡又多又好吃,蛋黃還是切丁拌入, 不是整顆放,吃起來「美味、均衡、飽滿」。一顆二十元。   老闆是陸軍專科十一期畢業取名專十一,...

韋伯連續劇終於更新 期待第一季順利完結

  地球天文學界的跳票大王詹姆斯·韋伯空間望遠鏡 (James Webb Space Telescope,縮寫為 JWST)自 1996 年以來斷斷續續不按劇本演出的連續劇終於讓焦慮的觀眾們又等到了一次更新:五層遮陽罩測試順利完成。 裝配完成的韋伯望遠鏡與好夥伴遮陽罩同框啦。Credit: NASA   嚴格的測試是任何空間任務順利成功的重中之重。遮陽罩,這個韋伯望遠鏡異常重要的親密夥伴,要是無法正常運轉的話,韋伯的這一季天文界連續劇說不準就要一直拖更了。   詹姆斯·韋伯空間望遠鏡是歷史上造出的最先進的空間望遠鏡。它不僅是一架紅外望遠鏡,還具有特別高的靈敏度。但想要達到辣么高的靈敏度來研究系外行星和遙遠的宇宙童年,韋伯童鞋必須非常"冷靜",體溫升高的話,靈敏度會大大折損。這個時候,遮陽罩就要大顯身手啦。   遮陽罩在韋伯的設計中至關重要。韋伯望遠鏡會被發射到拉格朗日 L2 點,運行軌道很高,遠離太陽、地球與月球。太陽是韋伯的主要熱量干擾的來源,其次是地球與月球。遮陽罩會有效阻斷來自這三大熱源的能量並保護韋伯維持在工作溫度正常運轉。這個工作溫度指的是零下 220 攝氏度(-370 華氏度;50 開爾文)。 上圖中我們可以看出,韋伯望遠鏡的配置大致可分為兩部分:紅色較熱的一面溫度為 85 攝氏度,藍色較冷的一面溫度達到零下 233 攝氏度。紅色的這部分中,儀器包括太陽能板、通信設備、計算機、以及轉向裝置。藍色部分的主要裝置包括鏡面、探測器、濾光片等。Credit: STSci.   遮陽罩的那一部分和望遠鏡的鏡面這部分可以產生非常極端的溫差。遮陽的這面溫度可以達到 110 攝氏度,足以煮熟雞蛋,而背陰處的部分溫度極低,足以凍結氧氣。   工程師們剛剛完成了五層遮陽罩的測試,按照韋伯在 L2 時的運行狀態安裝了遮陽罩。L2 距離地球約 160 萬公里。NASA 表示這些測試使用了航天器的自帶系統來展開遮陽罩,測試目前都已成功完成。韋伯望遠鏡遮陽罩負責人 James Cooper 介紹說這是遮陽罩"第一次在望遠鏡系統的电子設備的控制下展開。儘管這個任務非常艱巨,難度高,但測試順利完成,遮陽罩展開時的狀態非常驚艷"。   遮陽罩由五層 Kapton 製成。Kapton 是一種聚酰亞胺薄膜材料, 耐高溫絕...