Spring源碼學習筆記(二)web
前言----spring
最近花了些時間看了《Spring源碼深度解析》這本書,算是入門了Spring的源碼吧。打算寫下系列文章,回憶一下書的內容,總結代碼的運行流程。推薦那些和我同樣沒接觸過SSH框架源碼又想學習的,閱讀郝佳編著的《Spring源碼深度解析》這本書,會是個很好的入門。mvc
SpringMVC 的的入口就是 DispatcherServlet , 也是主要邏輯實現的地方。 先看看 DispatcherServlet 的 繼承關係。app
能夠看出 DispatcherServlet 也應該有和普通 Servlet 一樣的邏輯方法, 即 doDelete(), doGet(), doPost(), doPut() 以及 init(), destory() 等方法。框架
DispatcherServlet的初始化編輯器
初始化是調用 Servlet 的 init() 方法。ide
public final void init() throws ServletException { try { // 第一步: init-param的封裝 PropertyValues pvs = new HttpServletBean.ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties); //第二步: 封裝爲 BeanWrapper 類,Spring能夠把 獲取的 init-param 屬性注入 BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); //第三步: 針對 Resource 屬性 註冊屬性編輯器 ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext()); bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment())); this.initBeanWrapper(bw); //第四步: 添加屬性 bw.setPropertyValues(pvs, true); } catch (BeansException var4) { this.logger.error("Failed to set bean properties on servlet '" + this.getServletName() + "'", var4); throw var4; } // 第五步: 子類擴展 this.initServletBean(); }
在 init() 方法 第一步中, 封裝 初始化 參數, 找到一個內部類, 邏輯放在了構造函數中, 一段簡單清晰的代碼, 來一波!!! d=====( ̄▽ ̄*)b 函數
private static class ServletConfigPropertyValues extends MutablePropertyValues { public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties) throws ServletException { // 第一步: 根據須要的屬性, 生成迭代器, 並填充 Set<String> missingProps = requiredProperties != null && !requiredProperties.isEmpty()?new HashSet(requiredProperties):null; Enumeration en = config.getInitParameterNames(); while(en.hasMoreElements()) { String property = (String)en.nextElement(); Object value = config.getInitParameter(property); this.addPropertyValue(new PropertyValue(property, value)); //第二步: 填充的屬性要移出集合 if(missingProps != null) { missingProps.remove(property); } } //第三步: 遍歷完後還有未填充屬性, 須要拋異常 if(missingProps != null && missingProps.size() > 0) { throw new ServletException("Initialization from ServletConfig for servlet '" + config.getServletName() + "' failed; the following required properties were missing: " + StringUtils.collectionToDelimitedString(missingProps, ", ")); } } }
在 init()方法 第四步中, 注入的屬性包括 contextAttribute, contextClass, nameSpace, contextConfigLocation 等屬性, post
在 init()方法 第五步中, 父類 FrameworkServlet 定義了 initServletBean() 方法。 初始化了 ServletBean 。學習
protected final void initServletBean() throws ServletException { long startTime = System.currentTimeMillis(); try { //第一步: 去掉日誌, 忽略時間,剩下這句 this.webApplicationContext = this.initWebApplicationContext(); this.initFrameworkServlet(); } if(this.logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; this.logger.info("FrameworkServlet '" + this.getServletName() + "': initialization completed in " + elapsedTime + " ms"); } }
在 initServletBean() 方法中, 只有 建立或刷新 WebApplicationContext 實例的方法。
1 protected WebApplicationContext initWebApplicationContext() { 2 //第一步: 獲取context實例 3 WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext()); 4 WebApplicationContext wac = null; 5 if(this.webApplicationContext != null) { 6 //第二步: 判斷已在構造函數中注入 7 wac = this.webApplicationContext; 8 if(wac instanceof ConfigurableWebApplicationContext) { 9 ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac; 10 if(!cwac.isActive()) { 11 if(cwac.getParent() == null) { 12 cwac.setParent(rootContext); 13 } 14 15 //第三步: 刷新上下文 16 this.configureAndRefreshWebApplicationContext(cwac); 17 } 18 } 19 } 20 //第四步: 根據contextAttribute 屬性建立 context 21 if(wac == null) { 22 wac = this.findWebApplicationContext(); 23 } 24 //第五步: 仍是TMD 的木有, 從新建立一個 25 if(wac == null) { 26 wac = this.createWebApplicationContext(rootContext); 27 } 28 //第六步: context不是一個支持刷新的ConfigurableWebApplicationContext 或 構造函數注入的context已經被刷新 29 if(!this.refreshEventReceived) { 30 this.onRefresh(wac); 31 } 32 //第七步: 把context 實例 設置爲 contextAttributes的屬性 33 if(this.publishContext) { 34 String attrName = this.getServletContextAttributeName(); 35 this.getServletContext().setAttribute(attrName, wac); 36 if(this.logger.isDebugEnabled()) { 37 this.logger.debug("Published WebApplicationContext of servlet '" + this.getServletName() + "' as ServletContext attribute with name [" + attrName + "]"); 38 } 39 } 40 41 return wac; 42 }
在 initWebApplicationContext() 方法中, 第四步經過 contextAttribute 查找 context, 其中 contextAttribute 屬性在 web.xml 文件中配置, 默認爲
WebApplicationContext.class.getName() + ".ROOT'。
protected WebApplicationContext findWebApplicationContext() { //第一步: 獲取屬性 String attrName = this.getContextAttribute(); if(attrName == null) { return null; } else { //第二步: 根據屬性獲取 context WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext(), attrName); if(wac == null) { throw new IllegalStateException("No WebApplicationContext found: initializer not registered?"); } else { return wac; } } }
在 initWebApplicationContext() 方法中, 若 判斷 context 始終爲null, 則 經過 createWebApplicationContext() 方法建立新的 context 實例。
1 protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { 2 //往下調用方法 3 return this.createWebApplicationContext((ApplicationContext)parent); 4 }
1 protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) { 2 Class<?> contextClass = this.getContextClass(); 3 if(this.logger.isDebugEnabled()) { 4 this.logger.debug("Servlet with name '" + this.getServletName() + "' will try to create custom WebApplicationContext context of class '" + contextClass.getName() + "'" + ", using parent context [" + parent + "]"); 5 } 6 7 //第一步: 類型判斷 8 if(!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { 9 throw new ApplicationContextException("Fatal initialization error in servlet with name '" + this.getServletName() + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext"); 10 } else { 11 //第二步: 反射建立 context 12 ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass); 13 //第三步: 爲 context 設置屬性 14 wac.setEnvironment(this.getEnvironment()); 15 //第四步: parent 爲在ContextLoaderListener初始化時的 WebApplicationContext 實例 16 wac.setParent(parent); 17 wac.setConfigLocation(this.getContextConfigLocation()); 18 this.configureAndRefreshWebApplicationContext(wac); 19 return wac; 20 } 21 }
在 initWebApplicationContext() 方法第三步中, 不管經過構造函數 仍是 從新建立, 都會調用 this.configureAndRefreshWebApplicationContext(wac) 方法。 雖然看起來像給子類擴展的, 可是實際上 代碼不可貌相啊。 ----------------\(^o^)/!!!
1 protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) { 2 //第一步: 爲context 修改 id 3 if(ObjectUtils.identityToString(wac).equals(wac.getId())) { 4 if(this.contextId != null) { 5 wac.setId(this.contextId); 6 } else { 7 wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(this.getServletContext().getContextPath()) + "/" + this.getServletName()); 8 } 9 } 10 //第二步: 爲 context 填充屬性 11 wac.setServletContext(this.getServletContext()); 12 wac.setServletConfig(this.getServletConfig()); 13 wac.setNamespace(this.getNamespace()); 14 wac.addApplicationListener(new SourceFilteringListener(wac, new FrameworkServlet.ContextRefreshListener(null))); 15 ConfigurableEnvironment env = wac.getEnvironment(); 16 if(env instanceof ConfigurableWebEnvironment) { 17 ((ConfigurableWebEnvironment)env).initPropertySources(this.getServletContext(), this.getServletConfig()); 18 } 19 20 this.postProcessWebApplicationContext(wac); 21 this.applyInitializers(wac); 22 //第三步: 整合parent 到 context 23 wac.refresh(); 24 }
在 initWebApplicationContext() 方法第六步中, 調用 onRefresh() 方法, 初始化 一大堆的 Spring 的 web 功能必須的全局變量。 截圖感覺下, 在DispatcherServlet 中, o( ̄▽ ̄)d
在 DispatcherServlet 中, 重寫了父類 FrameworkServlet 的 onRefresh() 方法。
//第一步: 沒第一步 protected void onRefresh(ApplicationContext context) { this.initStrategies(context); } protected void initStrategies(ApplicationContext context) { this.initMultipartResolver(context); this.initLocaleResolver(context); this.initThemeResolver(context); this.initHandlerMappings(context); this.initHandlerAdapters(context); this.initHandlerExceptionResolvers(context); this.initRequestToViewNameTranslator(context); this.initViewResolvers(context); this.initFlashMapManager(context); }
介紹 DispatcherServlet 中的 全局變量的初始化工做, 打一個分隔行, 和上邊的基本沒什麼關係了!!! O(∩_∩)O加油!
強迫症, 再打一個。。。。
(一) 初始化 MultipartResolver
在 application-context.xml 文件中 配置一個 MutipartResolver 的Bean, 而後經過 Spring 容器去讀取就好了!!!
1 private void initMultipartResolver(ApplicationContext context) { 2 try { 3 //第一步: 讀取 bean 4 this.multipartResolver = (MultipartResolver)context.getBean("multipartResolver", MultipartResolver.class); 5 if(this.logger.isDebugEnabled()) { 6 this.logger.debug("Using MultipartResolver [" + this.multipartResolver + "]"); 7 } 8 } catch (NoSuchBeanDefinitionException var3) { 9 this.multipartResolver = null; 10 if(this.logger.isDebugEnabled()) { 11 this.logger.debug("Unable to locate MultipartResolver with name 'multipartResolver': no multipart request handling provided"); 12 } 13 } 14 15 }
(二) 初始化 LocaleResolver
和 MutipartResolver 差很少, 讀取 xml 文件中配置的 bean, 可是 LocaleResolver 能夠基於 URL, Session, Cookie來配置, 且有默認值。
1 private void initLocaleResolver(ApplicationContext context) { 2 try { 3 //第一步: 讀取配置的 bean 4 this.localeResolver = (LocaleResolver)context.getBean("localeResolver", LocaleResolver.class); 5 if(this.logger.isDebugEnabled()) { 6 this.logger.debug("Using LocaleResolver [" + this.localeResolver + "]"); 7 } 8 } catch (NoSuchBeanDefinitionException var3) { 9 //第二步: 生成默認對象 10 this.localeResolver = (LocaleResolver)this.getDefaultStrategy(context, LocaleResolver.class); 11 if(this.logger.isDebugEnabled()) { 12 this.logger.debug("Unable to locate LocaleResolver with name 'localeResolver': using default [" + this.localeResolver + "]"); 13 } 14 } 15 16 }
(三) 初始化 ThemeResolver
與 LocaleResolver 相同, 都有默認的 對象。
1 private void initThemeResolver(ApplicationContext context) { 2 try { 3 //第一步: 讀取bean 4 this.themeResolver = (ThemeResolver)context.getBean("themeResolver", ThemeResolver.class); 5 if(this.logger.isDebugEnabled()) { 6 this.logger.debug("Using ThemeResolver [" + this.themeResolver + "]"); 7 } 8 } catch (NoSuchBeanDefinitionException var3) { 9 //第二步: 生成默認對象 10 this.themeResolver = (ThemeResolver)this.getDefaultStrategy(context, ThemeResolver.class); 11 if(this.logger.isDebugEnabled()) { 12 this.logger.debug("Unable to locate ThemeResolver with name 'themeResolver': using default [" + this.themeResolver + "]"); 13 } 14 } 15 16 }
(四) 初始化 HandlerMappings
此處有套路!!!
1 private void initHandlerMappings(ApplicationContext context) { 2 this.handlerMappings = null; 3 if(this.detectAllHandlerMappings) { 4 //第一步: 獲取全部 HandlerMapping 類型的類 5 Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); 6 if(!matchingBeans.isEmpty()) { 7 this.handlerMappings = new ArrayList(matchingBeans.values()); 8 //第二步: 排序全部的 HandlerMapping 9 OrderComparator.sort(this.handlerMappings); 10 } 11 } else { 12 try { 13 //第三步: 獲取指定的 HandlerMapping, 返回一個 SingletonList 14 HandlerMapping hm = (HandlerMapping)context.getBean("handlerMapping", HandlerMapping.class); 15 this.handlerMappings = Collections.singletonList(hm); 16 } catch (NoSuchBeanDefinitionException var3) { 17 ; 18 } 19 } 20 21 if(this.handlerMappings == null) { 22 //第四步: 生成默認的 HandlerMapping 23 this.handlerMappings = this.getDefaultStrategies(context, HandlerMapping.class); 24 if(this.logger.isDebugEnabled()) { 25 this.logger.debug("No HandlerMappings found in servlet '" + this.getServletName() + "': using default"); 26 } 27 } 28 29 }
在 initHandlerMappings() 第四步中, 生成默認的 HandlerMapping 是經過讀取文件套路方法獲取的。
(五) 初始化 HandlerAdapters
也是使用模板套路方法, 和 HandlerMappings 相同。
1 private void initHandlerAdapters(ApplicationContext context) { 2 this.handlerAdapters = null; 3 if(this.detectAllHandlerAdapters) { 4 //第一步: 獲取全部的 HandlerAdapter 類型的類 5 Map<String, HandlerAdapter> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false); 6 if(!matchingBeans.isEmpty()) { 7 this.handlerAdapters = new ArrayList(matchingBeans.values()); 8 //第二步: 排序全部的 HandlerAdapter 9 OrderComparator.sort(this.handlerAdapters); 10 } 11 } else { 12 try { 13 //第三步: 獲取指定的 HandlerAdapter 14 HandlerAdapter ha = (HandlerAdapter)context.getBean("handlerAdapter", HandlerAdapter.class); 15 this.handlerAdapters = Collections.singletonList(ha); 16 } catch (NoSuchBeanDefinitionException var3) { 17 ; 18 } 19 } 20 21 if(this.handlerAdapters == null) { 22 //第四步: 生成默認的 HandlerAdapter 23 this.handlerAdapters = this.getDefaultStrategies(context, HandlerAdapter.class); 24 if(this.logger.isDebugEnabled()) { 25 this.logger.debug("No HandlerAdapters found in servlet '" + this.getServletName() + "': using default"); 26 } 27 } 28 29 }
在 initHandlerMappings() 第四步中 以及 在 initHandlerAdapters()的第四步中, getDefaultStrategies() 方法讀取 配置文件的 屬性。
1 protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) { 2 String key = strategyInterface.getName(); 3 //第一步: 嘗試從 defaultStrategies 中加載 屬性 4 String value = defaultStrategies.getProperty(key); 5 if(value == null) { 6 return new LinkedList(); 7 } else { 8 String[] classNames = StringUtils.commaDelimitedListToStringArray(value); 9 List<T> strategies = new ArrayList(classNames.length); 10 String[] var7 = classNames; 11 int var8 = classNames.length; 12 13 for(int var9 = 0; var9 < var8; ++var9) { 14 String className = var7[var9]; 15 16 try { 17 //第二步: 根據屬性對應的值,反射建立類實例 18 Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader()); 19 Object strategy = this.createDefaultStrategy(context, clazz); 20 strategies.add(strategy); 21 } 22 } 23 24 return strategies; 25 } 26 }
defaultStategies 在類初始化時加載了配置文件, 封裝了 屬性。
1 static { 2 try { 3 //第一步: 加載配置文件 4 ClassPathResource resource = new ClassPathResource("DispatcherServlet.properties", DispatcherServlet.class); 5 defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); 6 } catch (IOException var1) { 7 throw new IllegalStateException("Could not load 'DispatcherServlet.properties': " + var1.getMessage()); 8 } 9 }
一塊兒瞄一下配置文件, 也就是 DispatcherServlet.properties 的內容。
1 org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver 2 3 org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver 4 5 org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\ 6 org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping 7 8 org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\ 9 org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\ 10 org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter 11 12 org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\ 13 org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\ 14 org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver 15 16 org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator 17 18 org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver 19 20 org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
也就是在 properties 文件中, 定義了許多變量默認的類型, 若未定義, 則從 properties 文件中讀取對應的信息。
(六) 初始化 HandlerExceptionResolvers
和誰都不同, 不知道怎麼解釋, 上代碼吧, 簡單易懂
1 private void initHandlerExceptionResolvers(ApplicationContext context) { 2 this.handlerExceptionResolvers = null; 3 //第一步: 獲取全部的 HandlerExceptionResolver 類型的類 4 if(this.detectAllHandlerExceptionResolvers) { 5 Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false); 6 if(!matchingBeans.isEmpty()) { 7 this.handlerExceptionResolvers = new ArrayList(matchingBeans.values()); 8 //第二步: 排序全部的 HandlerExceptionResolver 9 OrderComparator.sort(this.handlerExceptionResolvers); 10 } 11 } else { 12 try { 13 //第三步: 獲取指定的 HandlerExceptionResolver 14 HandlerExceptionResolver her = (HandlerExceptionResolver)context.getBean("handlerExceptionResolver", HandlerExceptionResolver.class); 15 this.handlerExceptionResolvers = Collections.singletonList(her); 16 } catch (NoSuchBeanDefinitionException var3) { 17 ; 18 } 19 } 20 //第四步: 生成默認的對象 21 if(this.handlerExceptionResolvers == null) { 22 this.handlerExceptionResolvers = this.getDefaultStrategies(context, HandlerExceptionResolver.class); 23 if(this.logger.isDebugEnabled()) { 24 this.logger.debug("No HandlerExceptionResolvers found in servlet '" + this.getServletName() + "': using default"); 25 } 26 } 27 28 }
(七) 初始化 RequestToViewNameTranslator
此處省略解釋, 反正你也不聽!
1 private void initRequestToViewNameTranslator(ApplicationContext context) { 2 try { 3 //第一步: 讀取 xml 中定義的 bean 4 this.viewNameTranslator = (RequestToViewNameTranslator)context.getBean("viewNameTranslator", RequestToViewNameTranslator.class); 5 if(this.logger.isDebugEnabled()) { 6 this.logger.debug("Using RequestToViewNameTranslator [" + this.viewNameTranslator + "]"); 7 } 8 } catch (NoSuchBeanDefinitionException var3) { 9 //第二步: 生成默認的 RequestToViewNameTranslator 10 this.viewNameTranslator = (RequestToViewNameTranslator)this.getDefaultStrategy(context, RequestToViewNameTranslator.class); 11 if(this.logger.isDebugEnabled()) { 12 this.logger.debug("Unable to locate RequestToViewNameTranslator with name 'viewNameTranslator': using default [" + this.viewNameTranslator + "]"); 13 } 14 } 15 16 }
(八) 初始化 ViewResolvers
不聽不聽我不聽!!
1 private void initViewResolvers(ApplicationContext context) { 2 this.viewResolvers = null; 3 if(this.detectAllViewResolvers) { 4 //第一步: 獲取全部的 ViewResolver 類型的 bean 5 Map<String, ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false); 6 if(!matchingBeans.isEmpty()) { 7 this.viewResolvers = new ArrayList(matchingBeans.values()); 8 //第二步: 排序全部的 ViewResolver 9 OrderComparator.sort(this.viewResolvers); 10 } 11 } else { 12 try { 13 //第三步: 獲取指定的 ViewResolver 14 ViewResolver vr = (ViewResolver)context.getBean("viewResolver", ViewResolver.class); 15 this.viewResolvers = Collections.singletonList(vr); 16 } catch (NoSuchBeanDefinitionException var3) { 17 ; 18 } 19 } 20 //第四步: 生成默認的 ViewResolver 21 if(this.viewResolvers == null) { 22 this.viewResolvers = this.getDefaultStrategies(context, ViewResolver.class); 23 if(this.logger.isDebugEnabled()) { 24 this.logger.debug("No ViewResolvers found in servlet '" + this.getServletName() + "': using default"); 25 } 26 } 27 28 }
(九) 初始化 FlashMapManager
FlashMapManager 屬性瞭解得比較少。 簡單地說就是在重定向的時候保存屬性, 使重定向以後還能使用。 FlashMapManager 管理 FlashMap, FlashMap 保存了 flash attributes。
1 private void initFlashMapManager(ApplicationContext context) { 2 try { 3 //第一步: 讀取 xml 配置的 bean 4 this.flashMapManager = (FlashMapManager)context.getBean("flashMapManager", FlashMapManager.class); 5 if(this.logger.isDebugEnabled()) { 6 this.logger.debug("Using FlashMapManager [" + this.flashMapManager + "]"); 7 } 8 } catch (NoSuchBeanDefinitionException var3) { 9 //第二步: 生成默認的 FlashMapManager 10 this.flashMapManager = (FlashMapManager)this.getDefaultStrategy(context, FlashMapManager.class); 11 if(this.logger.isDebugEnabled()) { 12 this.logger.debug("Unable to locate FlashMapManager with name 'flashMapManager': using default [" + this.flashMapManager + "]"); 13 } 14 } 15 16 }
到此, DispatcherServlet 的初始化工做, 也就是 init() 方法結束, 接下來就是像日常使用 Servlet 同樣, 進行 service(), doGet(), doPost() 等方法的處理邏輯了。 <( ̄︶ ̄)>