10. BeanFactory Source Code Analysis of Spring

Keywords: Java Spring xml REST

BeanFactory Source Code Analysis for Spring (2)

Preface

In the previous section, we briefly analyzed the structure of BeanFactory, ListableBeanFactory, HierarchicalBeanFactory, AutowireCapableBeanFactory.DefaultListableBeanFactory, the main core class, gradually separates BeanFactory's functionality by programmatically starting the IOC container, making it easy for us to understand the entire architecture.

ClassPathResource resource  = new ClassPathResource("spring.xml");

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();

XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);

reader.loadBeanDefinitions(resource);

MyBean bean = factory.getBean(MyBean.class);

System.out.println(bean.toString());

DefaultListableBeanFactory implements the BeanDefinitionRegistry interface and has the ability to register bean s.

Reading resources is done through a separate module, where the XmlBeanDefinitionReader is delegated to read the xml configuration file

ApplicationContext

Previously, it was easy to programmatically control the creation of these configured containers, but in Spring, the system and the implementations of many containers have been defined. If BeanFactory is Spring's "heart", then the ApplicationContext is the complete "body".ApplicationContext is derived from BeanFactory and provides more practical functionality, so it is an IOC container with advanced morphological meaning. Let's take a closer look at it.

Take another look at the code:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");

MyBean bean = context.getBean(MyBean.class);

System.out.println(bean.toString());

Does this seem a lot simpler than DefaultListableBeanFactory? In fact, the so-called advanced container is the one that encapsulates the most basic container, so we started with the most basic BeanFactory to make it easier to understand.

Architecture

  • Supporting different information sources, we see the MessageSource interface of the ApplicationContext extensions, which can support internationalization and provide services for developing multilingual versions of applications.
  • Access resources.This feature is reflected in the support for ResourceLoader and Resource, so that we can get Bean Definition resources from different places.This abstraction allows users to define Bean definition information flexibly, especially from different I/O channels.
  • Supports application events.The interface ApplicationEventPublisher is inherited, introducing event mechanisms in the context.The combination of these events and Bean's life cycle facilitates Bean management.

Seeing the inheritance system above, you should be more aware that the ApplicationContext is a container for the advanced form of Spring BeanFactory.

Interface Method

@Nullable
    String getId();

    /**
     * Return a name for the deployed application that this context belongs to.
     * @return a name for the deployed application, or the empty String by default
     */
    String getApplicationName();

    /**
     * Return a friendly name for this context.
     * @return a display name for this context (never {@code null})
     */
    String getDisplayName();

    /**
     * Return the timestamp when this context was first loaded.
     * @return the timestamp (ms) when this context was first loaded
     */
    long getStartupDate();

    /**
     * Return the parent context, or {@code null} if there is no parent
     * and this is the root of the context hierarchy.
     * @return the parent context, or {@code null} if there is no parent
     */
    @Nullable
    ApplicationContext getParent();

    /**
     * Expose AutowireCapableBeanFactory functionality for this context.
     * <p>This is not typically used by application code, except for the purpose of
     * initializing bean instances that live outside of the application context,
     * applying the Spring bean lifecycle (fully or partly) to them.
     * <p>Alternatively, the internal BeanFactory exposed by the
     * {@link ConfigurableApplicationContext} interface offers access to the
     * {@link AutowireCapableBeanFactory} interface too. The present method mainly
     * serves as a convenient, specific facility on the ApplicationContext interface.
     * <p><b>NOTE: As of 4.2, this method will consistently throw IllegalStateException
     * after the application context has been closed.</b> In current Spring Framework
     * versions, only refreshable application contexts behave that way; as of 4.2,
     * all application context implementations will be required to comply.
     */
    AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;

In the ApplicationContext container, we analyze the design principle of the ApplicationContext container with the implementation of the commonly used ClassPathXmlApplicationContext

ClassPathXmlApplicationContext

Architecture

In the design of ClassPathXmlApplicationContext, its main function has been implemented in the class AbstractXmlApplicationContext.

Source Code Analysis

public ClassPathXmlApplicationContext(
            String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
            throws BeansException {

        super(parent);
        setConfigLocations(configLocations);
        if (refresh) {
            refresh();
        }
}

The ClassPathXmlApplicationContext still looks simple. The main implementations are implemented in the base class, which is responsible for the calls.

Setting of parent container in AbstractApplicationContext

/** Parent context */
@Nullable
private ApplicationContext parent;

@Override
public void setParent(@Nullable ApplicationContext parent) {
        this.parent = parent;
        if (parent != null) {
            Environment parentEnvironment = parent.getEnvironment();
            if (parentEnvironment instanceof ConfigurableEnvironment) {
                getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
            }
        }
}

If there is a parent container, the environment configuration environment for both is merged.Environment is not our focus here.

This refresh() process involves a series of complex operations initiated by the IOC container, and at the same time, these operations are similar for different container implementations, so they are encapsulated in the base class.So what we see in the design of the ClassPathXmlApplicationContext is a simple call.For the specific implementation of this refresh() at IOC container startup, this will be analyzed later and will not be expanded here.

Initialization of IOC Containers

Simply put, the initialization of the IOC container is initiated by the refresh() method described earlier, which marks the formal start of the IOC container.Specifically, this startup includes three basic processes: BeanDefinition's Result Location, Loading, and Registration.If we understand how to programmatically use IOC containers, we can clearly see the interface calls to the Resource locating and loading processes.In the following sections, we will analyze in detail the implementation of these three processes.

Spring separates these three processes and uses different modules to complete them, such as the responsive ReurceLoader, BeanDefinitionReader, and so on. This design allows users more flexibility to tailor or extend these three processes and define the initialization process that best suits their IOC containers.

  • Resource Location

    The first process is the Resource Location process.This Resource Location refers to the BeanDefinition's resource location, which is accomplished by the ResourceLoader through a unified Resource interface that provides a unified interface for all forms of BeanDefinition use.

  • Load of BeanDefinition

    The second process is loading the BeanDefinition.This loading process represents a user-defined Bean as a data structure inside an IOC container, which is a BeanDefinition. Specifically, this BeanDefinition is the abstraction of a POJO object in an IOC container. The data structure defined by this BeanDefinition enables the IOC container to easily manage POJO objects, which are also beans..

  • Register BeanDefinition

    The third process is to register these BeanDefinitions with the IOC container by calling the implementation of the BeanDefinitionRegistry interface.This registration process registers BeanDefinition resolved during loading to the IOC container. From the analysis, we can see that BeanDefinition is injected into a Concurrent HashMap inside the IOC container, through which the IOC container holds the BeanDefinition data.

Source Location for BeanDefinition

When using DefaultListableBeanFactory programmatically, you first define a Resource to locate the BeanDefinition used by the container.ClassPathResource is used, which means Spring will look for BeanDefinition information in the class path as a file.

ClassPathResource resource  = new ClassPathResource("spring.xml");

The Resource defined here is not directly used by the DefaultListableBeanFactory, and Spring processes this information through the BeanDefinitionReader.Here, we can also see the benefits of using the ApplicationContext over using the DefaultListableBeanFactory directly.Because in the ApplicationContext, Spring has provided us with a series of reader implementations that load different Resources, while DefaultListableBeanFactory is just a pure IOC container that requires specific readers to be configured to accomplish these functions.Of course, there are pros and cons. Using a lower-level container like DefaultListableBeanFactory can improve the flexibility of customizing IOC containers.

Using ClassPathXmlApplicationContext as an example, the implementation of this ApplicationContext is analyzed to see how it completes the Resource positioning process.

This ClassPathXmlApplicationContext already has the ability of ResourceLoader to read BeanDefinition defined by Resource by inheriting AbstractApplicationContext, because the base class of AbstractApplicationContext is DefaultResourceLOader.

In the previous source code for ClassPathXmlApplicationContext, we know that the core code is inside the refresh method.Here's refresh's code, but it won't be analyzed in detail. Our main work is to break it down. For now, we'll only analyze what we need.

AbstractApplicationContext -> refresh():

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        // Prepare this context for refreshing.
        prepareRefresh();

        // Tell the subclass to refresh the internal bean factory.
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

        // Prepare the bean factory for use in this context.
        prepareBeanFactory(beanFactory);

        try {
            // Allows post-processing of the bean factory in context subclasses.
            postProcessBeanFactory(beanFactory);

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

            // Register bean processors that intercept bean creation.
            registerBeanPostProcessors(beanFactory);

            // Initialize message source for this context.
            initMessageSource();

            // Initialize event multicaster for this context.
            initApplicationEventMulticaster();

            // Initialize other special beans in specific context subclasses.
            onRefresh();

            // Check for listener beans and register them.
            registerListeners();

            // Instantiate all remaining (non-lazy-init) singletons.
            finishBeanFactoryInitialization(beanFactory);

            // Last step: publish corresponding event.
            finishRefresh();
        }

        catch (BeansException ex) {
            if (logger.isWarnEnabled()) {
                logger.warn("Exception encountered during context initialization - " +
                            "cancelling refresh attempt: " + ex);
            }

            // Destroy already created singletons to avoid dangling resources.
            destroyBeans();

            // Reset 'active' flag.
            cancelRefresh(ex);

            // Propagate exception to caller.
            throw ex;
        }

        finally {
            // Reset common introspection caches in Spring's core, since we
            // might not ever need metadata for singleton beans anymore...
            resetCommonCaches();
        }
    }
}

Inside refresh is Spring's startup process, which creates a BeanFactory in refresh -> obtainFreshBeanFactory, which is implemented in the subclass AbstractRefreshableApplicationContext.

protected final void refreshBeanFactory() throws BeansException {
        if (hasBeanFactory()) {
            destroyBeans();
            closeBeanFactory();
        }
        try {
            DefaultListableBeanFactory beanFactory = createBeanFactory();
            beanFactory.setSerializationId(getId());
            customizeBeanFactory(beanFactory);
            //Load BeanDefinition, the rest of the methods will not be analyzed for now
            loadBeanDefinitions(beanFactory);
            synchronized (this.beanFactoryMonitor) {
                this.beanFactory = beanFactory;
            }
        }
        catch (IOException ex) {
            throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
        }
}

In this method, an IOC container is built by createBeanFactory for use by the ApplicationContext.This IOC container is the DefaultListableBeanFactory we mentioned earlier, and it also launches loadBeanDefinitions to load BeanDefinitions, which is very similar to the previous process of using IOC (XmlBeanFactory) programmatically.

AbstractXmlApplicationContext ->loadBeanDefinitions:

protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
    // Create a new XmlBeanDefinitionReader for the given BeanFactory.
    XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

    // Configure the bean definition reader with this context's
    // resource loading environment.
    beanDefinitionReader.setEnvironment(this.getEnvironment());
    beanDefinitionReader.setResourceLoader(this);
    beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

    // Allow a subclass to provide custom initialization of the reader,
    // then proceed with actually loading the bean definitions.
    initBeanDefinitionReader(beanDefinitionReader);
    loadBeanDefinitions(beanDefinitionReader);
}

Set BeanDefinitionReader because AbstractApplicationContext inherits DefaultResourceLoader, so ResourceLoader can be set to this to continue tracking code, in AbstractBeanDefinitionReader -> loadBeanDefinitions:

public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
        ResourceLoader resourceLoader = getResourceLoader();
        if (resourceLoader instanceof ResourcePatternResolver) {
            // Resource pattern matching available.
            try {
                Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
                //Analysis in Load and Analysis
                int loadCount = loadBeanDefinitions(resources);
                if (actualResources != null) {
                    for (Resource resource : resources) {
                        actualResources.add(resource);
                    }
                }
                return loadCount;
            }
            catch (IOException ex) {
                //...omitted
            }
        }
        else {
            //...omit code
        }
}
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);

Configuration resources are obtained through resourceLoader, which is the ClassPathXmlApplicationContext, a getResources method implemented in the parent AbstractApplicationContext

AbstractApplicationContext -> getResources:

public Resource[] getResources(String locationPattern) throws IOException {
    return this.resourcePatternResolver.getResources(locationPattern);
}

resourcePatternResolver was set to PathMatchingResourcePatternResolver at initialization

public AbstractApplicationContext() {
    this.resourcePatternResolver = getResourcePatternResolver();
}
protected ResourcePatternResolver getResourcePatternResolver() {
    return new PathMatchingResourcePatternResolver(this);
}   

This allows you to obtain resources through PathMatchingResourcePatternResolver.

Loading and parsing BeanDefinition

After completing the analysis of BeanDefinition's Resouurce location, here's how the entire BeanDefinition information is loaded.For IOC containers, this loading process is equivalent to converting a defined BeanDefinition into a data structure represented internally by Spring in the IOC container.The IOC container manages beans and relies on injection functionality through various operations on the BeanDefinition it holds.These BeanDefinition data are maintained and maintained in an IOC container through a HashMap.

Earlier, when locating resources, we showed the loadBeanDefinitions method in AbstractBeanDefinitionReader, where the following code is called:

int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException;

However, this method is not implemented in the AbstractBeanDefinitionReader class. It is an interface method. Specific implementation in XmlBeanDefinitionReader requires a resource representing an XML file in the reader, because this Resource object encapsulates I/O operations on the XML file, so the reader can get an XML file object after opening the I/O stream, with this pairOnce you have a file like this, you can parse the document tree of this XML according to Spring's Bean Definition Rule, which is handed over to BeanDefinitionParserDelegate.

XmlBeanDefinitionReader -> loadBeanDefinitions:

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {

    Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
    if (currentResources == null) {
        currentResources = new HashSet<>(4);
        this.resourcesCurrentlyBeingLoaded.set(currentResources);
    }
    if (!currentResources.add(encodedResource)) {
        //Omit Code
    }
    try {
        InputStream inputStream = encodedResource.getResource().getInputStream();
        try {
            InputSource inputSource = new InputSource(inputStream);
            if (encodedResource.getEncoding() != null) {
                inputSource.setEncoding(encodedResource.getEncoding());
            }
            return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
        }
        finally {
            inputStream.close();
        }
    }
    catch (IOException ex) {
        //Omit Code
    }
    finally {
        //Omit Code
    }
}

Next, look at the doLoadBeanDefinitions method:

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
    throws BeanDefinitionStoreException {
    try {
        //Get the Document object of the XML file
        Document doc = doLoadDocument(inputSource, resource);
        //The process of parsing BeanDefinition
        return registerBeanDefinitions(doc, resource);
    }
    //Omit some code

}

Instead of analyzing how to get the Document object, we are concerned with how Spring's BeanDefinion parses and translates into a container's internal data structure according to Spring's Bean semantic requirements, which is accomplished in registerBeanDefinitions.

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
    //Parsing BeanDefinition of XML through BeanDefinitionDocumentReader
    BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
    int countBefore = getRegistry().getBeanDefinitionCount();
    //Specific parsing process
    documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
    return getRegistry().getBeanDefinitionCount() - countBefore;
}

The loading of BeanDefinition is divided into two parts. Doument objects are first obtained by calling the parser of the XML, but they are not parsed according to Spring's Bean rules.Once you have finished parsing the generic XML, you will be able to parse it according to Spring's Bean rule, which is implemented in the documentReader, where the documentReader is the DefaultBean Definition document Reader that is configured by default.

protected BeanDefinitionDocumentReader createBeanDefinitionDocumentReader() {
        return BeanUtils.instantiateClass(this.documentReaderClass);
}
private Class<? extends BeanDefinitionDocumentReader> documentReaderClass =
            DefaultBeanDefinitionDocumentReader.class;

Configuration was resolved in DefaultBeanDefinitionDocumentReader-> parseDefaultElement

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

Configuration of bean s is parsed using the processBeanDefinition method

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
        // Extract the information from the < bean /> node and encapsulate it in a BeanDefinitionHolder
        BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
        if (bdHolder != null) {
            // If there are custom attributes, parse them accordingly, ignore them first
            bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
            try {
                //Here is registering BeanDefinition with the IOC container
                BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
            }
            catch (BeanDefinitionStoreException ex) {
                //Omit Code
            }
            //Send a message after BeanDefinition has registered with the IOC container
            getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
        }
}

The extracted information results are held by the BeanDefinitionHolder object.In addition to holding BeanDefinition objects, this BeanDefinition Holder holds other information related to the use of BeanDefinition, such as Bean's name, alias collection, and so on.

public class BeanDefinitionHolder implements BeanMetadataElement {

  private final BeanDefinition beanDefinition;

  private final String beanName;

  private final String[] aliases;
...

The specific interpretation of Spring BeanDefinition is done in BeanDefinition ParserDelegate, a class that contains the processing of rules defined by various Spring Beans, which we will not go into further here for the time being.

Registration of BeanDefinition in IOC Container

BeanDefinition loading and parsing in IOC containers has been previously analyzed.After these actions have been completed, the user-defined BeanDefinition information has built up its own data structure and corresponding data representation in the IOC container, but at this time the data cannot be used directly by the IOC container. The BeanDefinition data needs to be registered in the IOC container, which provides a more user-friendly way of using the IOC container, and in the DefaultListableIn BeanFactory, you hold the loaded BeanDefinition through a ConcurrentHashMap.

Register in DefaultBeanDefinition DocumentReader - >processBeanDefinition with the following code:

BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
public static void registerBeanDefinition(
            BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
            throws BeanDefinitionStoreException {

        // Register bean definition under primary name.
        String beanName = definitionHolder.getBeanName();
        
        // Regisry is DefaultListableBeanFactory
        registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

        // If there are aliases, also register them all once, otherwise the Bean will not be found.
        String[] aliases = definitionHolder.getAliases();
        if (aliases != null) {
            for (String alias : aliases) {
                // Alias -> beanName saves their alias information. It's easy to save it with a map.
               // When fetched, alias is converted to beanName before lookup
                registry.registerAlias(beanName, alias);
            }
        }
}

View the registerBeanDefinition method in DefaultListableBeanFactory:

public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
            throws BeanDefinitionStoreException {

        //Some code was omitted
        
        // Check if BeanDefinition already exists
        BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
        
        if (existingDefinition != null) {
            //If overwriting is not allowed
            if (!isAllowBeanDefinitionOverriding()) {
                //Throw an exception
            }
            else if (existingDefinition.getRole() < beanDefinition.getRole()) {
              
             //BeanDefinition analysis in Reference BeanFactory Source Analysis (1)

             //Overwrite user-defined beans with frame-defined beans 
            }
            else if (!beanDefinition.equals(existingDefinition)) {
             //Overwrite old beans with new beans
            }
            else {
             //Overwrite the old Bean with the same Bean, referring to the Bean whose equals method returns true
            }
            // cover
            this.beanDefinitionMap.put(beanName, beanDefinition);
        }
        else {
            // Determine if there are other beans already initialized.
            // Notice that the "Register Bean" action ends and the Bean is still not initialized
           if (hasBeanCreationStarted()) {
             // Cannot modify startup-time collection elements anymore (for stable iteration)
             synchronized (this.beanDefinitionMap) {
                this.beanDefinitionMap.put(beanName, beanDefinition);
                List<String> updatedDefinitions = new ArrayList<String>(this.beanDefinitionNames.size() + 1);
                updatedDefinitions.addAll(this.beanDefinitionNames);
                updatedDefinitions.add(beanName);
                this.beanDefinitionNames = updatedDefinitions;
                if (this.manualSingletonNames.contains(beanName)) {
                   Set<String> updatedSingletons = new LinkedHashSet<String>(this.manualSingletonNames);
                   updatedSingletons.remove(beanName);
                   this.manualSingletonNames = updatedSingletons;
                }
             }
          }
          else {
             // This branch is usually entered.
             // Put BeanDefinition in this map, which saves all BeanDefinitions
             this.beanDefinitionMap.put(beanName, beanDefinition);
             // This is an ArrayList, so the names of each registered Bean are saved in the order in which they are configured
             this.beanDefinitionNames.add(beanName);
             
             // This is a LinkedHashSet representing a manually registered singleton bean.
             // Note that this is the remove method, and Bean s coming here are certainly not registered manually
             // Manual refers to a bean registered by calling the following methods:
             // registerSingleton(String beanName, Object singletonObject)
             // That's not the point. Spring will "manually" register some beans later.
             // Bean s such as "environment", "systemProperties" can also be registered in containers at runtime by ourselves
             this.manualSingletonNames.remove(beanName);
          }
      this.frozenBeanDefinitionNames = null;
   }
}

Parts of the code have been omitted and the general process shown here is not covered here for specific details.Now that we have finished analyzing the loading and registration of BeanDefinition, Dependent Injection does not occur at this time. Dependent Injection occurs when a bean is first requested from a container by an application. Of course, the lazy-init property of the Bean can be set to control the pre-instantiation process. We will not analyze the Dependent Injection process here, but just roughly comb out the initialization process of the IOC container, which will be repeated again laterGo deeper into this section and dissect bit by bit.

summary

After analyzing the underlying BeanFactory, we analyzed the advanced form of the BeanFactory-ApplicationContext, which actually integrates other functions to make BeanFactory more than just a container. ApplicationContext has the following features:

  • Supports different sources of information
  • Accessing resources
  • Support application events

We then took ClassPathXmlApplicationContext as an example to briefly analyze BeanDefinition's resource positioning, loading and parsing, and registration processes.

  • Locate it through PathMatchingResourcePatternResolver.
  • The XmlBeanDefinitionReader reads the XML, reads the configuration information through the BeanDefinitionDocumentReader, and puts the configuration information into the BeanDefinitionHolder.
  • Register these BeanDefinition s in the DefaultListableBeanFactory.

Posted by Jaguar83 on Sat, 09 Nov 2019 00:45:06 -0800