Spring Java Config: คุณสร้าง @Bean ต้นแบบที่มีอาร์กิวเมนต์รันไทม์ได้อย่างไร


134

ด้วยการใช้ Java Config ของ Spring ฉันจำเป็นต้องได้รับ / สร้างอินสแตนซ์ถั่วที่กำหนดขอบเขตต้นแบบด้วยอาร์กิวเมนต์ตัวสร้างที่หาได้ในรันไทม์เท่านั้น พิจารณาตัวอย่างโค้ดต่อไปนี้ (ย่อให้สั้นลง):

@Autowired
private ApplicationContext appCtx;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = appCtx.getBean(Thing.class, name);

    //System.out.println(thing.getName()); //prints name
}

โดยที่คลาส Thing ถูกกำหนดไว้ดังนี้:

public class Thing {

    private final String name;

    @Autowired
    private SomeComponent someComponent;

    @Autowired
    private AnotherComponent anotherComponent;

    public Thing(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

ข้อสังเกตnameคือfinal: สามารถจัดหาผ่านตัวสร้างเท่านั้นและรับประกันความไม่เปลี่ยนรูป การอ้างอิงอื่น ๆ เป็นการอ้างอิงเฉพาะการนำไปใช้งานของThingคลาสและไม่ควรรู้ถึงการใช้ตัวจัดการคำร้องขอ

โค้ดนี้ทำงานได้ดีกับ Spring XML config ตัวอย่างเช่น:

<bean id="thing", class="com.whatever.Thing" scope="prototype">
    <!-- other post-instantiation properties omitted -->
</bean>

ฉันจะบรรลุสิ่งเดียวกันกับ Java config ได้อย่างไร สิ่งต่อไปนี้ใช้ไม่ได้กับ Spring 3.x:

@Bean
@Scope("prototype")
public Thing thing(String name) {
    return new Thing(name);
}

ตอนนี้ฉันสามารถสร้างโรงงานได้เช่น:

public interface ThingFactory {
    public Thing createThing(String name);
}

แต่นั่นเป็นการเอาชนะจุดทั้งหมดของการใช้ Spring เพื่อแทนที่รูปแบบการออกแบบ ServiceLocator และ Factoryซึ่งเหมาะอย่างยิ่งสำหรับกรณีการใช้งานนี้

หาก Spring Java Config สามารถทำได้ฉันจะสามารถหลีกเลี่ยง:

  • การกำหนดอินเทอร์เฟซจากโรงงาน
  • การกำหนดการใช้งานจากโรงงาน
  • การทดสอบการเขียนสำหรับการใช้งานในโรงงาน

นั่นเป็นงานมากมาย (ค่อนข้างพูด) สำหรับบางสิ่งที่ไม่สำคัญซึ่ง Spring รองรับผ่านการกำหนดค่า XML แล้ว


15
คำถามที่ยอดเยี่ยม
Sotirios Delimanolis

อย่างไรก็ตามมีเหตุผลใดที่คุณไม่สามารถสร้างอินสแตนซ์ของชั้นเรียนด้วยตัวเองและต้องได้รับจากฤดูใบไม้ผลิ? มีการพึ่งพาถั่วอื่น ๆ หรือไม่?
Sotirios Delimanolis

@SotiriosDelimanolis ใช่การThingใช้งานนั้นซับซ้อนกว่าและมีการพึ่งพาถั่วอื่น ๆ (ฉันแค่ละไว้เพื่อความกะทัดรัด) ด้วยเหตุนี้ฉันไม่ต้องการให้การใช้งานตัวจัดการคำขอทราบเกี่ยวกับสิ่งเหล่านี้เนื่องจากจะเป็นการจับคู่ตัวจัดการกับ API / ถั่วที่ไม่จำเป็นอย่างแน่นหนา ฉันจะอัปเดตคำถามเพื่อให้สอดคล้องกับคำถาม (ยอดเยี่ยม) ของคุณ
Les Hazlewood

ฉันไม่แน่ใจว่า Spring อนุญาตสิ่งนี้บนตัวสร้างหรือไม่ แต่ฉันรู้ว่าคุณสามารถใส่@Qualifierพารามิเตอร์ให้กับตัวตั้งค่าด้วย@Autowiredตัวเซ็ตเตอร์เอง
CodeChimp

2
ใน Spring 4 ตัวอย่างของคุณพร้อม@Beanผลงาน วิธีการได้รับเรียกว่ามีข้อโต้แย้งที่เหมาะสมที่คุณส่งผ่านไปยัง@Bean getBean(..)
Sotirios Delimanolis

คำตอบ:


94

ใน@Configurationชั้นเรียน@Beanวิธีการเช่นนั้น

@Bean
@Scope("prototype")
public Thing thing(String name) {
    return new Thing(name);
}

ถูกนำมาใช้ในการลงทะเบียนนิยามถั่วและให้โรงงานสำหรับการสร้างถั่ว ถั่วที่จะกำหนดเป็น instantiated ApplicationContextเฉพาะเมื่อข้อโต้แย้งคำขอใช้ที่ถูกกำหนดโดยตรงหรือผ่านการสแกน

ในกรณีของprototypebean อ็อบเจ็กต์ใหม่จะถูกสร้างขึ้นทุกครั้งดังนั้นจึงมี@Beanการเรียกใช้เมธอดที่เกี่ยวข้องด้วย

คุณสามารถเรียกถั่วจากที่ApplicationContextผ่านBeanFactory#getBean(String name, Object... args)วิธีการที่รัฐ

อนุญาตให้ระบุอาร์กิวเมนต์ตัวสร้างที่ชัดเจน / อาร์กิวเมนต์เมธอด factory แทนที่อาร์กิวเมนต์ดีฟอลต์ที่ระบุ (ถ้ามี) ในนิยาม bean

พารามิเตอร์:

อาร์กิวเมนต์ args ที่จะใช้หากสร้างต้นแบบโดยใช้อาร์กิวเมนต์ที่ชัดเจนสำหรับวิธีการโรงงานแบบคงที่ ไม่ถูกต้องที่จะใช้ค่า args ที่ไม่ใช่ค่าว่างในกรณีอื่น ๆ

กล่าวอีกนัยหนึ่งสำหรับprototypescoped bean คุณกำลังจัดเตรียมอาร์กิวเมนต์ที่จะใช้ไม่ใช่ในตัวสร้างของคลาส bean แต่อยู่ใน@Beanการเรียกใช้เมธอด

อย่างน้อยก็เป็นจริงสำหรับ Spring เวอร์ชัน 4+


12
ปัญหาของฉันกับวิธีนี้คือคุณไม่สามารถ จำกัด@Beanวิธีการเรียกใช้ด้วยตนเองได้ หากคุณเคย@Autowire Thingใช้@Beanวิธีนี้จะถูกเรียกว่ามีแนวโน้มที่จะตายเนื่องจากไม่สามารถฉีดพารามิเตอร์ได้ @Autowire List<Thing>เดียวกันหากคุณ ฉันพบว่าสิ่งนี้ค่อนข้างบอบบาง
ม.ค. Zyka

@JanZyka มีวิธีใดบ้างที่ฉันสามารถกำหนดค่าสิ่งต่างๆได้โดยอัตโนมัตินอกเหนือจากที่ระบุไว้ในคำตอบเหล่านี้ (ซึ่งโดยพื้นฐานแล้วจะเหมือนกันทั้งหมดหากคุณเหล่) โดยเฉพาะอย่างยิ่งถ้าฉันรู้ว่าข้อโต้แย้งล่วงหน้า (ที่รวบรวมที่ / เวลาการตั้งค่า) จะมีวิธีใด ๆ ที่ฉันสามารถแสดงความขัดแย้งเหล่านี้ในคำอธิบายประกอบบางอย่างที่ฉันสามารถมีคุณสมบัติ@Autowireด้วย?
M. Prokhorov

52

ด้วย Spring> 4.0 และ Java 8 คุณสามารถทำสิ่งนี้ได้อย่างปลอดภัยมากขึ้น:

@Configuration    
public class ServiceConfig {

    @Bean
    public Function<String, Thing> thingFactory() {
        return name -> thing(name); // or this::thing
    } 

    @Bean
    @Scope(value = "prototype")
    public Thing thing(String name) {
       return new Thing(name);
    }

}

การใช้งาน:

@Autowired
private Function<String, Thing> thingFactory;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = thingFactory.apply(name);

    // ...
}

ตอนนี้คุณสามารถรับ bean ของคุณได้ที่รันไทม์ นี่เป็นรูปแบบของโรงงานแน่นอน แต่คุณสามารถประหยัดเวลาในการเขียนคลาสเฉพาะเช่นThingFactory(อย่างไรก็ตามคุณจะต้องเขียนแบบกำหนดเอง@FunctionalInterfaceเพื่อส่งผ่านพารามิเตอร์มากกว่าสองพารามิเตอร์)


1
ฉันพบว่าแนวทางนี้มีประโยชน์และสะอาดมาก ขอบคุณ!
Alex Objelean

1
ผ้าคืออะไร? ฉันเข้าใจการใช้งานของคุณ .. แต่ไม่ใช่ศัพท์เฉพาะ .. ไม่คิดว่าจะเคยได้ยิน "ลายผ้า"
AnthonyJClink

1
@AnthonyJClink ฉันเดาว่าฉันเพิ่งใช้fabricแทนfactoryฉันไม่ดี :)
Roman Golyshev

1
@AbhijitSarkar โอ้ฉันเห็น แต่คุณไม่สามารถส่งผ่านพารามิเตอร์ไปยัง a Providerหรือไปยัง a ObjectFactoryหรือฉันผิด? และในตัวอย่างของฉันคุณสามารถส่งพารามิเตอร์สตริงไปยังมัน (หรือพารามิเตอร์ใดก็ได้)
Roman Golyshev

2
หากคุณไม่ต้องการ (หรือไม่จำเป็นต้อง) ใช้วิธีวงจรชีวิตของถั่วสปริง (ซึ่งแตกต่างจากถั่วต้นแบบ ... ) คุณสามารถข้าม@Beanและใส่ScopeคำอธิบายประกอบในThing thingวิธีการนี้ได้ ยิ่งไปกว่านั้นวิธีนี้สามารถทำให้เป็นส่วนตัวเพื่อซ่อนตัวเองและเหลือเพียงโรงงาน
m52509791

17

ตั้งแต่ฤดูใบไม้ผลิ 4.3 มีวิธีใหม่ในการทำซึ่งได้รับการแก้ไขสำหรับปัญหานั้น

ObjectProvider - ช่วยให้คุณสามารถเพิ่มเป็นการอ้างอิงกับถั่วต้นแบบที่ "โต้แย้ง" ของคุณและเพื่อสร้างอินสแตนซ์โดยใช้อาร์กิวเมนต์

นี่คือตัวอย่างง่ายๆในการใช้งาน:

@Configuration
public class MyConf {
    @Bean
    @Scope(BeanDefinition.SCOPE_PROTOTYPE)
    public MyPrototype createPrototype(String arg) {
        return new MyPrototype(arg);
    }
}

public class MyPrototype {
    private String arg;

    public MyPrototype(String arg) {
        this.arg = arg;
    }

    public void action() {
        System.out.println(arg);
    }
}


@Component
public class UsingMyPrototype {
    private ObjectProvider<MyPrototype> myPrototypeProvider;

    @Autowired
    public UsingMyPrototype(ObjectProvider<MyPrototype> myPrototypeProvider) {
        this.myPrototypeProvider = myPrototypeProvider;
    }

    public void usePrototype() {
        final MyPrototype myPrototype = myPrototypeProvider.getObject("hello");
        myPrototype.action();
    }
}

แน่นอนว่านี่จะพิมพ์สตริงสวัสดีเมื่อเรียกใช้ usePrototype


15

ปรับปรุงต่อความคิดเห็น

อันดับแรกฉันไม่แน่ใจว่าทำไมคุณถึงพูดว่า "ใช้ไม่ได้" สำหรับสิ่งที่ใช้ได้ดีใน Spring 3.x ฉันสงสัยว่าต้องมีบางอย่างผิดปกติในการกำหนดค่าของคุณที่ไหนสักแห่ง

ใช้งานได้:

- ไฟล์กำหนดค่า:

@Configuration
public class ServiceConfig {
    // only here to demo execution order
    private int count = 1;

    @Bean
    @Scope(value = "prototype")
    public TransferService myFirstService(String param) {
       System.out.println("value of count:" + count++);
       return new TransferServiceImpl(aSingletonBean(), param);
    }

    @Bean
    public AccountRepository aSingletonBean() {
        System.out.println("value of count:" + count++);
        return new InMemoryAccountRepository();
    }
}

- ไฟล์ทดสอบเพื่อดำเนินการ:

@Test
public void prototypeTest() {
    // create the spring container using the ServiceConfig @Configuration class
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ServiceConfig.class);
    Object singleton = ctx.getBean("aSingletonBean");
    System.out.println(singleton.toString());
    singleton = ctx.getBean("aSingletonBean");
    System.out.println(singleton.toString());
    TransferService transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter One");
    System.out.println(transferService.toString());
    transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter Two");
    System.out.println(transferService.toString());
}

การใช้ Spring 3.2.8 และ Java 7 ให้ผลลัพธ์นี้:

value of count:1
com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d
com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d
value of count:2
Using name value of: simulated Dynamic Parameter One
com.spring3demo.account.service.TransferServiceImpl@634d6f2c
value of count:3
Using name value of: simulated Dynamic Parameter Two
com.spring3demo.account.service.TransferServiceImpl@70bde4a2

ดังนั้นจึงขอ Bean 'Singleton' สองครั้ง อย่างไรก็ตามอย่างที่เราคาดหวัง Spring สร้างเพียงครั้งเดียว ครั้งที่สองจะเห็นว่ามีถั่วนั้นและส่งคืนวัตถุที่มีอยู่ ตัวสร้าง (วิธี @Bean) จะไม่ถูกเรียกเป็นครั้งที่สอง เมื่อเทียบกับสิ่งนี้เมื่อมีการร้องขอ 'Prototype' Bean จากอ็อบเจ็กต์บริบทเดียวกันสองครั้งเราจะเห็นว่าการอ้างอิงเปลี่ยนแปลงในเอาต์พุตและตัวสร้าง (@Bean method) ถูกเรียกสองครั้ง

คำถามคือจะฉีดซิงเกิลตันลงในต้นแบบได้อย่างไร คลาสการกำหนดค่าด้านบนแสดงวิธีการทำเช่นกัน! คุณควรส่งการอ้างอิงทั้งหมดดังกล่าวไปยังตัวสร้าง สิ่งนี้จะช่วยให้คลาสที่สร้างขึ้นเป็น POJO บริสุทธิ์และทำให้อ็อบเจ็กต์อ้างอิงที่มีอยู่ไม่เปลี่ยนรูปอย่างที่ควรจะเป็น ดังนั้นบริการรับส่งอาจมีลักษณะดังนี้:

public class TransferServiceImpl implements TransferService {

    private final String name;

    private final AccountRepository accountRepository;

    public TransferServiceImpl(AccountRepository accountRepository, String name) {
        this.name = name;
        // system out here is only because this is a dumb test usage
        System.out.println("Using name value of: " + this.name);

        this.accountRepository = accountRepository;
    }
    ....
}

หากคุณเขียนการทดสอบหน่วยคุณจะมีความสุขมากที่คุณสร้างชั้นเรียนนี้โดยไม่ต้องใช้ @ อัตโนมัติทั้งหมด หากคุณต้องการคอมโพเนนต์อัตโนมัติให้เก็บไว้ในไฟล์กำหนดค่า java

สิ่งนี้จะเรียกวิธีการด้านล่างใน BeanFactory หมายเหตุในคำอธิบายว่าสิ่งนี้มีไว้สำหรับกรณีการใช้งานของคุณอย่างไร

/**
 * Return an instance, which may be shared or independent, of the specified bean.
 * <p>Allows for specifying explicit constructor arguments / factory method arguments,
 * overriding the specified default arguments (if any) in the bean definition.
 * @param name the name of the bean to retrieve
 * @param args arguments to use if creating a prototype using explicit arguments to a
 * static factory method. It is invalid to use a non-null args value in any other case.
 * @return an instance of the bean
 * @throws NoSuchBeanDefinitionException if there is no such bean definition
 * @throws BeanDefinitionStoreException if arguments have been given but
 * the affected bean isn't a prototype
 * @throws BeansException if the bean could not be created
 * @since 2.5
 */
Object getBean(String name, Object... args) throws BeansException;

3
ขอบคุณสำหรับการตอบกลับ! อย่างไรก็ตามฉันคิดว่าคุณเข้าใจคำถามผิด ส่วนที่สำคัญที่สุดของคำถามคือต้องระบุค่ารันไทม์เป็นอาร์กิวเมนต์ตัวสร้างเมื่อได้รับ (สร้างอินสแตนซ์) ต้นแบบ
Les Hazlewood

ฉันอัปเดตการตอบกลับของฉัน อันที่จริงดูเหมือนว่าการจัดการค่ารันไทม์ทำได้อย่างถูกต้องดังนั้นฉันจึงปล่อยส่วนนั้นออกไป ได้รับการสนับสนุนอย่างชัดเจนแม้ว่าคุณจะเห็นได้จากการอัปเดตและผลลัพธ์จากโปรแกรม
JoeG

0

คุณสามารถบรรลุเอฟเฟกต์ที่คล้ายกันได้โดยใช้คลาสภายใน :

@Component
class ThingFactory {
    private final SomeBean someBean;

    ThingFactory(SomeBean someBean) {
        this.someBean = someBean;
    }

    Thing getInstance(String name) {
        return new Thing(name);
    }

    class Thing {
        private final String name;

        Thing(String name) {
            this.name = name;
        }

        void foo() {
            System.out.format("My name is %s and I can " +
                    "access bean from outer class %s", name, someBean);
        }
    }
}


-1

ตอบช้าด้วยวิธีการที่แตกต่างกันเล็กน้อย นั่นคือการติดตามของคำถามล่าสุดนี้ที่อ้างถึงคำถามนี้เอง

ใช่ตามที่กล่าวไว้คุณสามารถประกาศ bean ต้นแบบที่ยอมรับพารามิเตอร์ใน@Configurationคลาสที่อนุญาตให้สร้าง bean ใหม่ในการฉีดแต่ละครั้ง
นั่นจะทำให้@Configuration ชั้นนี้เป็นโรงงานและเพื่อไม่ให้โรงงานนี้มีความรับผิดชอบมากเกินไปก็ไม่ควรรวมถั่วอื่น ๆ

@Configuration    
public class ServiceFactory {

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Thing thing(String name) {
       return new Thing(name);
   }

}

แต่คุณยังสามารถฉีด configuration bean นั้นเพื่อสร้างThings:

@Autowired
private ServiceFactory serviceFactory;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = serviceFactory.thing(name); // create a new bean at each invocation
    // ...    
}

มีทั้งแบบปลอดภัยและรัดกุม


1
ขอบคุณสำหรับคำตอบ แต่นี่เป็นรูปแบบการต่อต้านสปริง ออบเจ็กต์กำหนดค่าไม่ควร 'รั่วไหล' ลงในโค้ดของแอปพลิเคชัน - มีอยู่เพื่อกำหนดค่ากราฟอ็อบเจ็กต์แอปพลิเคชันของคุณและส่วนต่อประสานกับโครงสร้าง Spring นี่คล้ายกับคลาส XML ในถั่วแอปพลิเคชันของคุณ (เช่นกลไกการกำหนดค่าอื่น) นั่นคือหาก Spring มาพร้อมกับกลไกการกำหนดค่าอื่นคุณจะต้อง refactor รหัสแอปพลิเคชันของคุณซึ่งเป็นตัวบ่งชี้ที่ชัดเจนว่าสิ่งนี้ละเมิดการแยกข้อกังวล เป็นการดีกว่าที่จะให้ Config ของคุณสร้างอินสแตนซ์ของอินเทอร์เฟซ Factory / Function และฉีด Factory โดยไม่ต้องมีการเชื่อมต่อแบบแน่นกับ config
Les Hazlewood

1) ฉันยอมรับอย่างสมบูรณ์ว่าในกรณีทั่วไปออบเจ็กต์การกำหนดค่าจะต้องไม่รั่วไหลออกจากสนาม แต่ในกรณีเฉพาะนี้การฉีดออบเจ็กต์การกำหนดค่าที่กำหนดถั่วเพียงหนึ่งเดียวเพื่อผลิตถั่วต้นแบบนั้น IHMO มีความหมายอย่างสมบูรณ์: คลาสการกำหนดค่านี้กลายเป็นโรงงาน ปัญหาการแยกความกังวลอยู่ที่ไหนถ้าทำให้เป็นเพียงนั้น? ...
davidxxx

... 2) เกี่ยวกับ "นั่นคือถ้า Spring มาพร้อมกับกลไกการกำหนดค่าอื่น" มันเป็นข้อโต้แย้งที่ไม่ถูกต้องเพราะเมื่อคุณตัดสินใจที่จะใช้กรอบงานในแอปพลิเคชันของคุณคุณจะจับคู่แอปพลิเคชันของคุณกับสิ่งนั้น ดังนั้นไม่ว่าในกรณีใดคุณจะต้องทำการ refactor แอปพลิเคชัน Spring ที่ขึ้นอยู่กับ@Configurationว่ากลไกนั้นเปลี่ยนไปหรือไม่
davidxxx

1
... 3) BeanFactory#getBean()คำตอบที่คุณยอมรับได้เสนอที่จะใช้ แต่นั่นแย่กว่าในแง่ของการมีเพศสัมพันธ์เนื่องจากเป็นโรงงานที่อนุญาตให้รับ / สร้างอินสแตนซ์ถั่วใด ๆ ของแอปพลิเคชันและไม่เพียง แต่ถั่วชนิดใดที่ต้องการในปัจจุบัน ด้วยการใช้งานดังกล่าวคุณสามารถผสมผสานความรับผิดชอบในชั้นเรียนของคุณได้อย่างง่ายดายเนื่องจากการอ้างอิงที่อาจดึงได้ไม่ จำกัด ซึ่งไม่แนะนำ แต่เป็นกรณีพิเศษ
davidxxx

@ davidxxx - ฉันยอมรับคำตอบเมื่อหลายปีก่อนก่อนที่ JDK 8 และ Spring 4 จะเป็นไปโดยพฤตินัย คำตอบของโรมันข้างต้นนั้นถูกต้องกว่าสำหรับประเพณีสปริงสมัยใหม่ เกี่ยวกับคำแถลงของคุณ "เนื่องจากเมื่อคุณตัดสินใจใช้เฟรมเวิร์กในแอปพลิเคชันของคุณคุณจะจับคู่แอปพลิเคชันของคุณด้วย" ค่อนข้างตรงข้ามกับคำแนะนำของทีม Spring และแนวทางปฏิบัติที่ดีที่สุดของ Java Config - ถาม Josh Long หรือ Jeurgen Hoeller หากคุณได้รับ โอกาสที่จะพูดคุยกับพวกเขาด้วยตนเอง (ฉันมีและฉันมั่นใจได้ว่าพวกเขาแนะนำอย่างชัดเจนว่าอย่าจับคู่รหัสแอปของคุณกับ Spring ทุกครั้งที่ทำได้) ไชโย
Les Hazlewood
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.