Can I replace a Spring bean definition at runtime?

后端 未结 11 1355
萌比男神i
萌比男神i 2020-11-30 17:44

Consider the following scenario. I have a Spring application context with a bean whose properties should be configurable, think DataSource or MailSender

相关标签:
11条回答
  • 2020-11-30 18:30

    You can create a custom scope called "reconfigurable" into the ApplicationContext. It creates and caches instances of all beans in this scope. On a configuration change it clears the cache and re-creates the beans on first access with the new configuration. For this to work you need to wrap all instances of reconfigurable beans into an AOP scoped proxy, and access the configuration values with Spring-EL: put a map called config into the ApplicationContext and access the configuration like #{ config['key'] }.

    0 讨论(0)
  • 2020-11-30 18:32

    My solution was to copy the original object. Fist i created an interface

    /**
     * Allows updating data to some object.
     * Its an alternative to {@link Cloneable} when you cannot 
     * replace the original pointer. Ex.: Beans 
     * @param <T> Type of Object
     */
    public interface Updateable<T>
    {
        /**
         * Import data from another object
         * @param originalObject Object with the original data
         */
        public void copyObject(T originalObject);
    }
    

    For easing the implementation of the function fist create a constructor with all fields, so the IDE could help me a bit. Then you can make a copy constructor that uses the same function Updateable#copyObject(T originalObject). You can also profit of the code of the constructor created by the IDE to create the function to implement:

    public class SettingsDTO implements Cloneable, Updateable<SettingsDTO>
    {
        private static final Logger LOG = LoggerFactory.getLogger(SettingsDTO.class);
    
        @Size(min = 3, max = 30)  
        private String id;
    
        @Size(min = 3, max = 30)
        @NotNull 
        private String name;
    
        @Size(min = 3, max = 100)
        @NotNull 
        private String description;
    
        @Max(100)
        @Min(5) 
        @NotNull
        private Integer pageSize;
    
        @NotNull 
        private String dateFormat; 
    
        public SettingsDTO()
        { 
        }   
    
        public SettingsDTO(String id, String name, String description, Integer pageSize, String dateFormat)
        {
            this.id = id;
            this.name = name;
            this.description = description;
            this.pageSize = pageSize;
            this.dateFormat = dateFormat;
        }
    
        public SettingsDTO(SettingsDTO original)
        {
            copyObject(original);
        }
    
        @Override
        public void copyObject(SettingsDTO originalObject)
        {
            this.id = originalObject.id;
            this.name = originalObject.name;
            this.description = originalObject.description;
            this.pageSize = originalObject.pageSize;
            this.dateFormat = originalObject.dateFormat;
        } 
    }
    

    I used it in a Controller for updating the current settings for the app:

            if (bindingResult.hasErrors())
            {
                model.addAttribute("settingsData", newSettingsData);
                model.addAttribute(Templates.MSG_ERROR, "The entered data has errors");
            }
            else
            {
                synchronized (settingsData)
                {
                    currentSettingData.copyObject(newSettingsData);
                    redirectAttributes.addFlashAttribute(Templates.MSG_SUCCESS, "The system configuration has been updated successfully");
                    return String.format("redirect:/%s", getDao().getPath());
                }
            }
    

    So the currentSettingsData which has the configuration of the application gonna have the updated values, located in newSettingsData. These method allows updating any bean without high complexity.

    0 讨论(0)
  • 2020-11-30 18:38

    Here is how I have done it in the past: running services which depend on configuration which can be changed on the fly implement a lifecycle interface: IRefreshable:

    public interface IRefreshable {
      // Refresh the service having it apply its new values.
      public void refresh(String filter);
    
      // The service must decide if it wants a cache refresh based on the refresh message filter.
      public boolean requiresRefresh(String filter);
    }
    

    Controllers (or services) which can modify a piece of configuration broadcast to a JMS topic that the configuration has changed (supplying the name of the configuration object). A message driven bean then invokes the IRefreshable interface contract on all beans which implement IRefreshable.

    The nice thing with spring is that you can automatically detect any service in your application context that needs to be refreshed, removing the need to explicitly configure them:

    public class MyCacheSynchService implements InitializingBean, ApplicationContextAware {
     public void afterPropertiesSet() throws Exception {
      Map<String, ?> refreshableServices = m_appCtx.getBeansOfType(IRefreshable.class);
      for (Map.Entry<String, ?> entry : refreshableServices.entrySet() ) {
       Object beanRef = entry.getValue();
       if (beanRef instanceof IRefreshable) {
        m_refreshableServices.add((IRefreshable)beanRef);
       }
      }
     }
    }
    

    This approach works particularly well in a clustered application where one of many app servers might change the configuration, which all then need to be aware of. If you want to use JMX as the mechanism for triggering the changes, your JMX bean can then broadcast to the JMS topic when any of its attributes are changed.

    0 讨论(0)
  • 2020-11-30 18:39

    Further updated answer to cover scripted bean

    Another approach supported by spring 2.5.x+ is that of the scripted bean. You can use a variety of languages for your script - BeanShell is probably the most intuitive given that it has the same syntax as Java, but it does require some external dependencies. However, the examples are in Groovy.

    Section 24.3.1.2 of the Spring Documentation covers how to configure this, but here are some salient excerpts illustrating the approach which I've edited to make them more applicable to your situation:

    <beans>
    
        <!-- This bean is now 'refreshable' due to the presence of the 'refresh-check-delay' attribute -->
        <lang:groovy id="messenger"
              refresh-check-delay="5000" <!-- switches refreshing on with 5 seconds between checks -->
              script-source="classpath:Messenger.groovy">
            <lang:property name="message" value="defaultMessage" />
        </lang:groovy>
    
        <bean id="service" class="org.example.DefaultService">
            <property name="messenger" ref="messenger" />
        </bean>
    
    </beans>
    

    With the Groovy script looking like this:

    package org.example
    
    class GroovyMessenger implements Messenger {
    
        private String message = "anotherProperty";
    
        public String getMessage() {
            return message;
        }
    
        public void setMessage(String message) {
            this.message = message
        }
    }
    

    As the system administrator wants to make changes then they (or you) can edit the contents of the script appropriately. The script is not part of the deployed application and can reference a known file location (or one that is configured through a standard PropertyPlaceholderConfigurer during startup).

    Although the example uses a Groovy class, you could have the class execute code that reads a simple properties file. In that manner, you never edit the script directly, just touch it to change the timestamp. That action then triggers the reload, which in turn triggers the refresh of properties from the (updated) properties file, which finally updates the values within the Spring context and off you go.

    The documentation does point out that this technique doesn't work for constructor-injection, but maybe you can work around that.

    Updated answer to cover dynamic property changes

    Quoting from this article, which provides full source code, one approach is:

    * a factory bean that detects file system changes
    * an observer pattern for Properties, so that file system changes can be propagated
    * a property placeholder configurer that remembers where which placeholders were used, and updates singleton beans’ properties
    * a timer that triggers the regular check for changed files
    

    The observer pattern is implemented by the interfaces and classes ReloadableProperties, ReloadablePropertiesListener, PropertiesReloadedEvent, and ReloadablePropertiesBase. None of them are especially exciting, just normal listener handling. The class DelegatingProperties serves to transparently exchange the current properties when properties are updated. We only update the whole property map at once, so that the application can avoid inconsistent intermediate states (more on this later).

    Now the ReloadablePropertiesFactoryBean can be written to create a ReloadableProperties instance (instead of a Properties instance, as the PropertiesFactoryBean does). When prompted to do so, the RPFB checks file modification times, and if necessary, updates its ReloadableProperties. This triggers the observer pattern machinery.

    In our case, the only listener is the ReloadingPropertyPlaceholderConfigurer. It behaves just like a standard spring PropertyPlaceholderConfigurer, except that it tracks all usages of placeholders. Now when properties are reloaded, all usages of each modified property are found, and the properties of those singleton beans are assigned again.

    Original answer below covering static property changes:

    Sounds like you just want to inject external properties into your Spring context. The PropertyPlaceholderConfigurer is designed for this purpose:

      <!-- Property configuration (if required) -->
      <bean id="serverProperties" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
          <list>
            <!-- Identical properties in later files overwrite earlier ones in this list -->
            <value>file:/some/admin/location/application.properties</value>
          </list>
        </property>
      </bean>
    

    you then reference the external properties with Ant syntax placeholders (that can be nested if you want from Spring 2.5.5 onwards)

      <bean id="example" class="org.example.DataSource">
        <property name="password" value="${password}"/>
      </bean>
    

    You then ensure that the application.properties file is only accessible to the admin user and the user running the application.

    Example application.properties:

    password=Aardvark

    0 讨论(0)
  • 2020-11-30 18:39

    Or you could use the approach from this similar question and hence also my solution:

    The approach is to have beans that are configured via property files and the solution is to either

    • refresh the entire applicationContext (automatically using a scheduled task or manually using JMX) when properties have changed or
    • use a dedicated property provider object to access all properties. This property provider will keep checking the properties files for modification. For beans where prototype-based property lookup is impossible, register a custom event that your property provider will fire when it finds an updated property file. Your beans with complicated lifecycles will need to listen for that event and refresh themselves.
    0 讨论(0)
提交回复
热议问题