从返回的匿名内部类调用@Bean方法的线程安全性

时间:2012-10-03 00:12:36

标签: java spring thread-safety config

以下测试方法是否是线程安全的,假设这是从多个线程调用的?有时println语句显示“null”。

我们的想法是返回一个地图,该地图将根据提供的标识符按需创建bean。请注意,这只是一个简单的示例,用于说明类似的现实生活场景,其中一个bean使用相同的方法不满足其依赖关系(例如,value.x为null)。对于奖励积分,还有另一种(更好的)方法可以达到同样的效果吗?

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;

import com.oanda.bi.rm.test.AnnotationConfigTest.Config;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { Config.class }, loader = AnnotationConfigContextLoader.class)
public class AnnotationConfigTest {

    @Resource
    Map<String, Value> map;

    @Test
    public void test() throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool( 10 );

        for ( int i = 0; i < 10; i++ ) {
            service.execute( new Runnable() {
                @Override
                public void run() {
                    // Is this thread-safe?
                    Value value = map.get( "value" );

                    // Sometimes null!
                    System.out.println( value.x );
                }
            } );
        }

        service.shutdown();
        service.awaitTermination( 1, TimeUnit.MINUTES );
    }

    public static class Value {
        @Resource
        protected Integer x;
    }

    @Configuration
    public static class Config {

        @Bean
        public Integer x() {
            return 1;
        }

        @Bean
        @Scope("prototype")
        public Value value() {
            return new Value();
        }

        @Bean
        @SuppressWarnings("serial")
        public Map<String, Value> map() {
            // Return a Spring agnostic "bean factory" map
            return Collections.unmodifiableMap( new HashMap<String, Value>() {
                @Override
                public Value get( Object obj ) {
                    String key = (String) obj;

                    if ( key.equals( "value" ) ) {
                        // Create new bean on demand
                        return value();
                    }

                    // Assume other similar branches here...

                    return null;
                }
            } );
        }

    }
}

更新

鉴于来自Biju Kunjummen的深刻反馈,我尝试了一种直接使用应用程序上下文的不同方法,但它仍然失败将为null。这次我使用的是函数抽象而不是Map,这似乎更合适:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javax.annotation.Resource;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;

import com.google.common.base.Function;
import com.oanda.bi.rm.test.AnnotationFunctionConfigTest.Config;

/**
 * Unit test that tries to perform the same pattern as {#link ResourceConfig} and ensure
 * thread safety.
 * 
 * @see http://stackoverflow.com/questions/12700239/thread-safety-of-calling-bean-methods-from-returned-annonymous-inner-classes/12700284#comment17146235_12700284
 * @see http://forum.springsource.org/showthread.php?130731-Thread-safety-of-calling-Bean-methods-from-returned-annonymous-inner-classes&p=426403#post426403
 * @author btiernay
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { Config.class }, loader = AnnotationConfigContextLoader.class)
public class AnnotationFunctionConfigTest {

    @Resource
    Function<String, Value> function;

    @Test
    public void test() throws InterruptedException {
        final int threads = 10;
        ExecutorService service = Executors.newFixedThreadPool( threads );

        for ( int i = 0; i < threads; i++ ) {
            service.execute( new Runnable() {
                @Override
                public void run() {
                    Value value = function.apply( "value" );
                    Assert.assertNotNull( value.x );
                }
            } );
        }

        service.shutdown();
        service.awaitTermination( 1, TimeUnit.MINUTES );
    }

    public static class Value {
        @Resource
        protected Integer x;
    }

    @Configuration
    public static class Config {

        @Bean
        public Integer x() {
            return 1;
        }

        @Bean
        @Scope("prototype")
        public Value value() {
            return new Value();
        }

        @Bean
        public Function<String, Value> function() {
            // Return a Spring agnostic "bean factory" function
            return new Function<String, Value>() {
                @Autowired
                private ApplicationContext applicationContext;

                @Override
                public Value apply( String key ) {
                    if ( key.equals( "value" ) ) {
                        // Create new bean on demand
                        return applicationContext.getBean( key, Value.class );
                    }

                    // Assume other similar branches here...

                    return null;
                }
            };
        }

    }
}

任何人都在关注为什么这似乎仍然不安全?

更新

这看起来可能是一个Spring bug。我提交了一张jira票:

https://jira.springsource.org/browse/SPR-9852

2 个答案:

答案 0 :(得分:2)

我不建议您实施它的方式:

  1. 就像Jordan说的那样,那里的地图并不是真的需要,你根本就没有把它用作散列图,只是用它来调用.value()方法。

  2. 正在绕过Spring @Configuration机制,内部Spring为@Configuration类创建一个CGLIB代理,并使用它知道需要在哪里注入哪些依赖项并创建知道如何管理作用域的实例,绕过它你基本上不再使用Spring来管理你的bean实例了。

  3. 以下内容类似于您已实现的内容,但我认为更简单并且每次都能保持一致 - 这是通过使用应用程序上下文来获取原型bean并将其隐藏在自定义工厂后面的方式:

    更新实施

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.BeansException;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Scope;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration
    public class AnnotationConfigTest {
    
    
        @Autowired PrototypeBeanFactory prototypeFactory;
    
        @Test
        public void test() throws InterruptedException {
            ExecutorService service = Executors.newFixedThreadPool( 10 );
    
            for ( int i = 0; i < 10; i++ ) {
                service.execute( new Runnable() {
                    @Override
                    public void run() {
                        Value value = prototypeFactory.getBean("value", Value.class);
                        System.out.println( "value1.x = "  + value.getX() );
                    }
                } );
            }
    
            service.shutdown();
            service.awaitTermination( 1, TimeUnit.MINUTES );
        }
    
        public static class Value {
            @Autowired
            private Integer x;
    
            public Integer getX() {
                return x;
            }
    
            public void setX(Integer x) {
                this.x = x;
            }
        }
    
    
    
        @Configuration
        public static class Config {
    
            @Bean
            public Integer x() {
                return 1;
            }
    
            @Bean
            @Scope(value="prototype")
            public Value value() {
                return new Value();
            }
    
            @Bean
            public PrototypeBeanFactory prototypeFactory(){
                return new PrototypeBeanFactory();
            }
    
        }
    
    
        public static class PrototypeBeanFactory implements ApplicationContextAware{
            private ApplicationContext applicationContext;
            @Override
            public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
                this.applicationContext = applicationContext;
            }
    
            public<T> T getBean(String name, Class<T> clazz){
                return this.applicationContext.getBean(name, clazz);
            }
    
        }
    }
    

答案 1 :(得分:0)

你所做的只是返回一个只有get方法的不可变映射,所以真正唯一的方法就是修改传递的obj的值。更好的解决方案可能是使用ConcurrentHashMap而不是Collections.unmodifiableMap,但如果您计划稍后向地图添加值,那么这只会非常有用。