การเรียกเมธอด Spring @Transaction โดยเมธอดภายในคลาสเดียวกันไม่ได้ผลใช่หรือไม่


111

ฉันเพิ่งเริ่มทำธุรกรรม Spring มีบางอย่างที่ฉันพบว่าแปลกจริงๆฉันอาจเข้าใจเรื่องนี้อย่างถูกต้อง

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

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

นี่คือรหัส:

public class UserService {

    @Transactional
    public boolean addUser(String userName, String password) {
        try {
            // call DAO layer and adds to database.
        } catch (Throwable e) {
            TransactionAspectSupport.currentTransactionStatus()
                    .setRollbackOnly();

        }
    }

    public boolean addUsers(List<User> users) {
        for (User user : users) {
            addUser(user.getUserName, user.getPassword);
        }
    } 
}

ดูTransactionTemplateวิธีการ: stackoverflow.com/a/52989925/355438
Lu55

เกี่ยวกับสาเหตุที่การเรียกด้วยตนเองไม่ได้ผลโปรดดูที่8.6 กลไกการพร็อกซี
Jason Law

คำตอบ:


100

เป็นข้อ จำกัด ของSpring AOP (วัตถุแบบไดนามิกและcglib )

หากคุณกำหนดค่า Spring ให้ใช้AspectJเพื่อจัดการธุรกรรมรหัสของคุณจะทำงาน

ทางเลือกที่ง่ายและน่าจะดีที่สุดคือการ refactor โค้ดของคุณ ตัวอย่างเช่นคลาสหนึ่งที่จัดการผู้ใช้และอีกคลาสที่ประมวลผลผู้ใช้แต่ละคน จากนั้นการจัดการธุรกรรมเริ่มต้นด้วย Spring AOP จะทำงาน


เคล็ดลับการกำหนดค่าสำหรับการจัดการธุรกรรมด้วย AspectJ

ในการเปิดใช้งาน Spring เพื่อใช้ AspectJ สำหรับธุรกรรมคุณต้องตั้งค่าโหมดเป็น AspectJ:

<tx:annotation-driven mode="aspectj"/>

หากคุณใช้ Spring เวอร์ชันเก่ากว่า 3.0 คุณต้องเพิ่มสิ่งนี้ในการกำหนดค่า Spring ของคุณด้วย:

<bean class="org.springframework.transaction.aspectj
        .AnnotationTransactionAspect" factory-method="aspectOf">
    <property name="transactionManager" ref="transactionManager" />
</bean>

ขอขอบคุณสำหรับข้อมูล. ตอนนี้ฉันปรับรหัสใหม่แล้ว แต่คุณช่วยส่งตัวอย่างโดยใช้ AspectJ หรือให้ลิงค์ที่เป็นประโยชน์แก่ฉันได้ไหม ขอบคุณล่วงหน้า. ไมค์.
Mike

เพิ่มการกำหนดค่า AspectJ เฉพาะธุรกรรมในคำตอบของฉัน ฉันหวังว่ามันจะช่วยได้
Espen

11
ดีแล้ว! Btw: คงจะดีถ้าคุณสามารถทำเครื่องหมายคำถามของฉันเป็นคำตอบที่ดีที่สุดเพื่อให้ฉันได้คะแนน (เครื่องหมายถูกสีเขียว)
Espen

2
Spring boot config: @EnableTransactionManagement (mode = AdviceMode.ASPECTJ)
VinyJones

64

ปัญหาคือพร็อกซี AOP ของ Spring ไม่ขยาย แต่จะรวมอินสแตนซ์บริการของคุณเพื่อดักฟังการโทร สิ่งนี้มีผลคือการเรียกไปยัง "this" จากภายในอินสแตนซ์บริการของคุณจะถูกเรียกโดยตรงบนอินสแตนซ์นั้นและไม่สามารถดักจับได้โดยพร็อกซีการตัด (พร็อกซีไม่ทราบถึงการเรียกดังกล่าว) วิธีแก้ปัญหาหนึ่งได้กล่าวไว้แล้ว สิ่งที่ดีอีกประการหนึ่งคือเพียงแค่ให้ Spring แทรกอินสแตนซ์ของบริการลงในบริการเองและเรียกใช้วิธีการของคุณในอินสแตนซ์ที่แทรกซึ่งจะเป็นพร็อกซีที่จัดการธุรกรรมของคุณ แต่โปรดทราบว่าสิ่งนี้อาจมีผลข้างเคียงที่ไม่ดีเช่นกันหากถั่วบริการของคุณไม่ใช่ซิงเกิลตัน:

<bean id="userService" class="your.package.UserService">
  <property name="self" ref="userService" />
    ...
</bean>

public class UserService {
    private UserService self;

    public void setSelf(UserService self) {
        this.self = self;
    }

    @Transactional
    public boolean addUser(String userName, String password) {
        try {
        // call DAO layer and adds to database.
        } catch (Throwable e) {
            TransactionAspectSupport.currentTransactionStatus()
                .setRollbackOnly();

        }
    }

    public boolean addUsers(List<User> users) {
        for (User user : users) {
            self.addUser(user.getUserName, user.getPassword);
        }
    } 
}

3
หากคุณเลือกที่จะไปเส้นทางนี้ (ไม่ว่าจะเป็นการออกแบบที่ดีหรือไม่ก็เป็นอีกเรื่องหนึ่ง) และไม่ใช้การฉีดตัวสร้างตรวจสอบให้แน่ใจว่าคุณเห็นคำถามนี้ด้วย
Jeshurun

จะเกิดอะไรขึ้นถ้าUserServiceมีขอบเขตซิงเกิลตัน? ถ้าเป็นวัตถุเดียวกันล่ะ?
Yan Khonski

26

ด้วย Spring 4 เป็นไปได้ที่จะทำงานอัตโนมัติด้วยตนเอง

@Service
@Transactional
public class UserServiceImpl implements UserService{
    @Autowired
    private  UserRepository repository;

    @Autowired
    private UserService userService;

    @Override
    public void update(int id){
       repository.findOne(id).setName("ddd");
    }

    @Override
    public void save(Users user) {
        repository.save(user);
        userService.update(1);
    }
}

2
คำตอบที่ดีที่สุด !! Thx
mjassani

2
แก้ไขฉันถ้าฉันผิด แต่รูปแบบดังกล่าวมีโอกาสเกิดข้อผิดพลาดได้ง่ายแม้ว่าจะใช้งานได้ มันเหมือนการแสดงความสามารถของ Spring มากกว่าใช่มั้ย? ผู้ที่ไม่คุ้นเคยกับพฤติกรรม "การเรียกถั่วนี้" อาจนำถั่วที่ทำงานอัตโนมัติออกโดยไม่ได้ตั้งใจ (วิธีนี้มีอยู่ใน "สิ่งนี้") ซึ่งอาจทำให้เกิดปัญหาที่ตรวจพบได้ยากในตอนแรก มันสามารถทำให้มันอยู่ในสภาพแวดล้อมที่แยงก่อนที่จะพบ)
pidabrow

2
@pidabrow คุณพูดถูกมันเป็นรูปแบบการต่อต้านที่ยิ่งใหญ่และไม่ชัดเจนในตอนแรก ดังนั้นหากคุณทำได้คุณควรหลีกเลี่ยง หากคุณต้องใช้วิธีการของคลาสเดียวกันลองใช้ไลบรารี AOP ที่มีประสิทธิภาพมากขึ้นเช่น AspectJ
Almas Abdrazak

24

เริ่มจาก Java 8 มีความเป็นไปได้อื่นซึ่งฉันชอบด้วยเหตุผลที่ระบุด้านล่าง:

@Service
public class UserService {

    @Autowired
    private TransactionHandler transactionHandler;

    public boolean addUsers(List<User> users) {
        for (User user : users) {
            transactionHandler.runInTransaction(() -> addUser(user.getUsername, user.getPassword));
        }
    }

    private boolean addUser(String username, String password) {
        // TODO
    }
}

@Service
public class TransactionHandler {

    @Transactional(propagation = Propagation.REQUIRED)
    public <T> T runInTransaction(Supplier<T> supplier) {
        return supplier.get();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public <T> T runInNewTransaction(Supplier<T> supplier) {
        return supplier.get();
    }
}

แนวทางนี้มีข้อดีดังต่อไปนี้:

1) อาจใช้กับวิธีการส่วนตัว คุณจึงไม่ต้องทำลายการห่อหุ้มโดยการทำให้วิธีการเป็นสาธารณะเพียงเพื่อตอบสนองข้อ จำกัด ของ Spring

2) วิธีการเดียวกันอาจถูกเรียกภายในการเผยแพร่ธุรกรรมที่แตกต่างกันและขึ้นอยู่กับผู้โทรที่จะเลือกวิธีที่เหมาะสม เปรียบเทียบ 2 บรรทัดนี้:

transactionHandler.runInTransaction(() -> userService.addUser(user.getUserName, user.getPassword));
transactionHandler.runInNewTransaction(() -> userService.addUser(user.getUserName, user.getPassword));

3) มีความชัดเจนจึงอ่านได้มากขึ้น


1
นี่มันเยี่ยมมาก! หลีกเลี่ยงข้อผิดพลาดทั้งหมดที่ Spring แนะนำด้วยคำอธิบายประกอบเป็นอย่างอื่น รักเลย!
Frank Hopkins

ถ้าฉันขยายTransactionHandlerเป็นคลาสย่อยและคลาสย่อยเรียกใช้สองวิธีนี้ในTransactionHandlerซูเปอร์คลาสฉันจะยังคงได้รับประโยชน์@Transactionalตามที่ตั้งใจไว้หรือไม่
tom_mai78101

ฟังดูวิเศษมาก! ฉันสงสัยว่ามีข้อแม้บางประการหรือไม่?
ch271828n

6

นี่คือทางออกของฉันสำหรับการเรียกร้องด้วยตนเอง :

public class SBMWSBL {
    private SBMWSBL self;

    @Autowired
    private ApplicationContext applicationContext;

    @PostConstruct
    public void postContruct(){
        self = applicationContext.getBean(SBMWSBL.class);
    }

    // ...
}

0

คุณสามารถทำงานอัตโนมัติ BeanFactory ภายในคลาสเดียวกันและทำไฟล์

getBean(YourClazz.class)

มันจะประกาศชั้นเรียนของคุณโดยอัตโนมัติและคำนึงถึงคำอธิบายประกอบ @Transactional หรือ aop อื่น ๆ ของคุณ


2
ถือเป็นการปฏิบัติที่ไม่ดี แม้แต่การฉีดถั่วซ้ำเข้าไปในตัวเองก็ยังดีกว่า การใช้ getBean (clazz) เป็นการเชื่อมต่อที่แน่นหนาและการพึ่งพาคลาส Spring ApplicationContext ภายในโค้ดของคุณ นอกจากนี้การรับถั่วตามชั้นเรียนอาจไม่ได้ผลในกรณีที่ห่อถั่วด้วยสปริง (คลาสอาจมีการเปลี่ยนแปลง)
Vadim Kirilchuk

0

ปัญหานี้เกี่ยวข้องกับวิธีการสปริงโหลดคลาสและพร็อกซี มันจะไม่ได้ผลจนกว่าคุณจะเขียนวิธีการ / ธุรกรรมภายในของคุณในคลาสอื่นหรือไปที่คลาสอื่นจากนั้นมาที่คลาสของคุณอีกครั้งจากนั้นเขียนเมธอดการแปลงที่ซ้อนกันภายใน

สรุปได้ว่า Spring proxies ไม่อนุญาตให้เกิดสถานการณ์ที่คุณกำลังเผชิญอยู่ คุณต้องเขียนวิธีการทำธุรกรรมที่ 2 ในคลาสอื่น


0

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

@Service
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
class PersonDao {

    private final PersonDao _personDao;

    @Autowired
    public PersonDao(PersonDao personDao) {
        _personDao = personDao;
    }

    @Transactional
    public void addUser(String username, String password) {
        // call database layer
    }

    public void addUsers(List<User> users) {
        for (User user : users) {
            _personDao.addUser(user.getUserName, user.getPassword);
        }
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.