Today’s sharing started, please give us more advice ~

Why does Spring’s @Transaction annotation not work for your database transactions? Transactions that are not perfect can be difficult to detect during development and testing because you can’t allow for too many unexpected scenarios. However, when the development of business data volume, it may lead to a large number of inconsistent data, which will cause people to plant trees and people to step on holes, requiring a lot of manpower to solve problems and repair data.

Many scenarios that require transactions simply add a @Transactional annotation to a method

But do you think that’s really enough?

How do I verify that a Spring transaction is valid?

Transactional: Open a Transactional Transactional with the @Transactional key. There are always “surprises” in trusting frameworks too much. Consider the following example:

Domain layer

The user entity

DAO layer

Field service

  • CreateUserError1 calls the private method

  • CreateUserPrivate, annotated by @Transactional. If the passed user name contains test, an exception is thrown and the user creation operation fails

  • getUserCount

User interface layer

Call UserService# createUserError1

  • Result Even if the user name is invalid, the user can be created successfully. Refresh the browser, repeatedly found more than a dozen illegal user registration.

The @transactional principle works

Public methods

Unless specifically configured (such as implementing AOP using AspectJ static weaving), @Transactional must be defined in a public method to take effect.

Because of Spring’s AOP, private methods cannot be propped in, and naturally cannot dynamically enhance transaction processing logic.

Just change the createUserPrivate method to public.

However, it was found that the transaction still did not take effect.

The target method must be invoked externally from a proxied class

Invoking the enhanced method must be the object after invoking the proxy.

Try modifying the UserService by injecting a self and then calling the createUserPublic method marked with the @Transactional annotation through the self instance. If you set a breakpoint, you can see that self is a CGLIB class enhanced by Spring:

CGLIB implements proxy classes by inheritance, and private methods are not visible in subclasses, so transaction enhancement is not possible. While the this pointer represents the calling object itself, Spring cannot inject this, so accessing the method through this must not be a proxy.

Change this to self to verify that the transaction is in effect: illegal user registration operations can be rolled back.

While injecting itself inside the UserDomainService and calling its own createUserPublic will correctly implement the transaction, this is not normal. It makes more sense to have the Controller call the createUserPublic method of the UserService defined earlier.

This call UserDomainService/self/Controller

  • Since this call

Unable to walk to the Spring proxy class

  • The latter two

Call spring-injected UserService, which has the opportunity to dynamically enhance the createUserPublic method through a proxy call.

It is recommended to turn on Debug logging at development time to learn the details of Spring transaction implementation.

For example, enable Debug logging for JPA database access:

logging.level.org.springframework.orm.jpa=DEBUG

After logging is enabled, compare this in UserService with createUserPublic in Controller through the injected UserService Bean.

The transaction does not take effect in createUserPublic and only takes effect in Repository’s Save:

This implementation is cumbersome for handling exceptions in a Controller. It is better to annotate createUserWrong2 with the @Transactional annotation and call this method directly in the Controller.

This allows the UserService method to be called externally (from the Controller), while the method is public and can be enhanced by dynamic proxy AOP.

summary

Be sure to call methods marked by the @Transactional annotation that are decorated by public and are called from a Spring-injected Bean.

However, sometimes a transaction may not be rolled back even if it is effective because exceptions are not handled correctly.

Just because a transaction is in effect does not mean it can be rolled back correctly

AOP implements transactions: a method that wraps the @Transactional annotation with a try/catch:

  • A transaction rollback can be set in a catch when the method fails and certain conditions are met
  • If there is no exception, commit the transaction directly

Certain conditions

A transaction can be rolled back only if the exception propagates a method annotated by @Transactional.

Spring’s TransactionAspectSupport#invokeWithinTransaction deals with transactions. A look at the source code shows that only after an exception is caught can subsequent transactions occur:

By default, Spring rolls back the transaction only when a RuntimeException or Error occurs.

Spring DefaultTransactionAttribute:

  • Checked exceptions are typically business exceptions or similar return values from another method that may still complete the business, so no active rollback is performed

  • Error or RuntimeException represents an unexpected result

Events that can’t be rolled back properly

Exceptions cannot propagate out of methods

B. abnormal

During registration, a file is read. If the file fails to be read, the registered DB operation is expected to be rolled back. CreateUserError2 createUserError2 createUserError2

Although the above method avoids the pit where the transaction does not take effect, the transaction is still not rolled back due to improper exception handling.

Fix the rollback failure bug

1 Manually set the rollback state of the current office

If you want to catch exceptions and handle them yourself, you can manually set the rollback state of the current office

View the log, and the transaction confirms the rollback.

Transactional code has requested rollback: Manual rollback.

Note 2 states that all exceptions are expected to roll back the transaction

  • Breaking the default limit of not rolling back checked exceptions

  • View logs, prompting rollback:

This case has DB operations, IO operations, and expects DB transactions to be rolled back in case of IO operation problems to ensure logical consistency.

Because of incorrect exception handling, the transaction was in effect but was not rolled back when an exception occurred.

Spring’s default is to roll back only @Transactional annotated methods when runtimeExceptions and errors occur, so if the method catches an exception, the transaction rollback needs to be handled by hand-written code.

If you want Spring to rollback against other exceptions, configure the @Transactional annotation’s rollbackFor and noRollbackFor properties to override Spring’s default configuration.

Some businesses may contain multiple DB operations and do not necessarily want to treat the two operations as a single transaction, so you need to carefully consider the configuration of transaction propagation.

3 Check whether the transaction propagation configuration complies with service logic

case

User registration: a primary user is inserted into the user table, and an associated subuser is registered. The DB operation of the child user registration is expected to be a separate transaction, so that failure does not affect the process of registering the primary user.

  • UserService: Creates primary and sub-users

  • SubUserService: fails to register subusers. The child user registration is expected to be rolled back as a single transaction without affecting the registered primary user

  • View the log after initiating the call: the transaction rolled back

Wrong! Since the runtime exception escapes createUserWrong annotated by @Transactional, Spring of course rolls back the transaction. Catch exceptions thrown by child methods if the primary method is not expected to roll back.

Revised plan

The catch on the subUserService# createSubUserWithExceptionError package, it won’t appear abnormal outer createUserError2 main method

Viewing logs after startup Notice:

  • Enable exception handling for createUserError2
  • The child method marks the current transaction as rollback due to a runtime exception
  • The main method catches the exception and prints a Create Sub User error
  • The main method commits the transaction

But a UnexpectedRollbackException Controller, abnormal describe tip end the transaction rolled back and the silent rollback: because of createUserError2 itself is no exception, just found the child after submit method has been set to rollback the current transaction, unable to complete the submission.

A transaction may not be committed even though no exception has occurred

Because the logic of registering the master user for the master method and the logic of registering the child user for the submethod are the same transaction, the sublogic marks the transaction to be rolled back, and the master logic cannot commit naturally.

The fix is clear: separate sublogical transactions, which fixes the SubUserService registration subuser method and sets the REQUIRES_NEW transaction propagation policy for annotations adding propagation = Propagation.requiRES_new. That is, when this method is executed, a new transaction is opened and the current transaction is suspended.

Creates a new transaction and suspends the current transaction if it exists. Similar to the EJB transaction property of the same name.

Note: Actual transaction pauses are not unboxed outside of all transaction managers. This is especially suitable for org. Springframework. Transaction. The jta. JtaTransactionManager, This would require the javax.mail. Transaction. TransactionManager is provided to it (this is the server specific standard Java EE)

Create createUserRight (createUserRight, createUserRight, createUserRight);

Then check the log

  • Creating new transaction with name createUserRight

Enable the main method transaction for createUserRight

  • createMainUser finish

The primary user is created

  • Suspending current transaction, creating new transaction with name createSubUserWithExceptionRight

The main transaction pending, open a new transaction, namely the createSubUserWithExceptionRight create child user logic

  • Initiating transaction rollback

Submethod transaction rollback

  • Resuming suspended transaction after completion of inner transaction

The submethod transaction completes, resuming the transaction that was suspended before the main method

  • create sub user error:invalid status

The primary method caught an exception for the child method

  • Committing JPA transaction on EntityManager

The transaction for the main method commits, and then we see no silent rollback exception in the Controller

If a method involves multiple DB operations and you want to commit or roll back them as separate transactions, consider refining the transaction Propagation mode by configuring the Propagation attribute of the @Transactional annotation.

conclusion

To enable transactions for private methods, dynamic proxy AOP is not feasible and requires static weaving, which means weaving transaction-enhancing code at compile time. The Spring framework can be configured to implement AOP using AspectJ.

Today’s share has ended, please forgive and give advice!