I am following this article to implement a database read/write separation feature by calling different methods. However, I got the error:
Missing method call for verify(mock) here: verify(spyDatabaseContextHolder, times(1)).set(DatabaseEnvironment.READONLY);
when doing the testing.
My test case is trying to verify DatabaseEnvironment.READONLY has been set once when using TransactionReadonlyAspect AOP annotation:
// TransactionReadonlyAspectTest.java
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = {LoadServiceImpl.class, TransactionReadonlyAspect.class})
public class TransactionReadonlyAspectTest {
#Autowired
private TransactionReadonlyAspect transactionReadonlyAspect;
#MockBean
private LoadServiceImpl loadService;
#Test
public void testReadOnlyTransaction() throws Throwable {
ProceedingJoinPoint mockProceedingJoinPoint = mock(ProceedingJoinPoint.class);
Transactional mockTransactional = mock(Transactional.class);
DatabaseContextHolder spyDatabaseContextHolder = mock(DatabaseContextHolder.class);
when(mockTransactional.readOnly()).thenReturn(true);
when(loadService.findById(16)).thenReturn(null);
when(mockProceedingJoinPoint.proceed()).thenAnswer(invocation -> loadService.findById(16));
transactionReadonlyAspect.proceed(mockProceedingJoinPoint, mockTransactional);
verify(spyDatabaseContextHolder, times(1)).set(DatabaseEnvironment.READONLY); // got the error: Missing method call for verify(mock)
verify(loadService, times(1)).findById(16);
assertEquals(DatabaseContextHolder.getEnvironment(), DatabaseEnvironment.UPDATABLE);
}
}
//TransactionReadonlyAspect.java
#Aspect
#Component
#Order(0)
#Slf4j
public class TransactionReadonlyAspect {
#Around("#annotation(transactional)")
public Object proceed(ProceedingJoinPoint proceedingJoinPoint,
org.springframework.transaction.annotation.Transactional transactional) throws Throwable {
try {
if (transactional.readOnly()) {
log.info("Inside method " + proceedingJoinPoint.getSignature());
DatabaseContextHolder.set(DatabaseEnvironment.READONLY);
}
return proceedingJoinPoint.proceed();
} finally {
DatabaseContextHolder.reset();
}
}
}
// DatabaseContextHolder.java
public class DatabaseContextHolder {
private static final ThreadLocal<DatabaseEnvironment> CONTEXT = new ThreadLocal<>();
public static void set(DatabaseEnvironment databaseEnvironment) {
CONTEXT.set(databaseEnvironment);
}
public static DatabaseEnvironment getEnvironment() {
DatabaseEnvironment context = CONTEXT.get();
System.out.println("context: " + context);
return CONTEXT.get();
}
public static void reset() {
CONTEXT.set(DatabaseEnvironment.UPDATABLE);
}
}
//DatabaseEnvironment.java
public enum DatabaseEnvironment {
UPDATABLE,READONLY
}
// LoadServiceImpl.java
#Service
public class LoadServiceImpl implements LoadService {
#Override
#Transactional(readOnly = true)
public LoadEntity findById(Integer Id) {
return this.loadDAO.findById(Id);
}
...
}
I just want to test DatabaseContextHolder.set(DatabaseEnvironment.READONLY) has been used once then in the TransactionReadonlyAspect finally block it will be reset to DatabaseEnvironment.UPDATABLE which make sense.
However, how to test DatabaseContextHolder.set(DatabaseEnvironment.READONLY) gets called once? Why does this error occur? Is there a better way to test TransactionReadonlyAspect?
Spring Transaction doesn't support multi-thread, so I try to manage transaction manually in Thread's run() method. But, It doesn't work!
I'd like to rollback each thread's run() method in below example when there's a exception throw inside it. (In below case, INSERT INTO UNKNOWN_TABLE)
My expected result is 'start, 1, 3, 5, end'.
And the actual result is 'start, 1, 2, 3, 4, 5, end'.
Any reply is welcome! Thanks!
Main Class:
#SpringBootApplication
public class Application implements CommandLineRunner {
#Autowired
private TestService testService;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public DriverManagerDataSource createDriverManagerDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("oracle.jdbc.driver.OracleDriver");
dataSource.setJdbcUrl("jdbc:oracle:thin:#url:port/schema");
dataSource.setUsername("xxxx");
dataSource.setPassword("xxxx");
return dataSource;
}
#Bean
public JdbcTemplate createJdbcTemplate() {
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(createDriverManagerDataSource());
return jdbcTemplate;
}
#Override
public void run(String... args) throws Exception {
testService.test();
}
}
Service Class:
#Service
public class TestService {
#Autowired
private JdbcTemplate jdbcTemplate;
#Transactional(rollbackFor = Exception.class)
public void test() throws Exception {
jdbcTemplate.batchUpdate("INSERT INTO TB_MYTEST(MYKEY, MYVALUE) VALUES ('start', 'start')");
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 1; i <= 5; i++) {
executorService.submit(new TestRunner(i));
}
executorService.shutdown();
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
jdbcTemplate.batchUpdate("INSERT INTO TB_MYTEST(MYKEY, MYVALUE) VALUES ('end', 'end')");
}
private class TestRunner implements Runnable {
private Integer id;
public TestRunner(Integer id) {
this.id = id;
}
#Override
public void run() {
try (Connection connection = jdbcTemplate.getDataSource().getConnection()) {
try {
connection.setAutoCommit(false);
String sqlString = String.format("INSERT INTO TB_MYTEST(MYKEY, MYVALUE) VALUES ('%d', '%d')", id, id);
jdbcTemplate.batchUpdate(sqlString);
if (id % 2 == 0) {
// Except the transaction been rollback when this.id is 2 or 4.
jdbcTemplate.batchUpdate("INSERT INTO UNKNOWN_TABLE(MYKEY, MYVALUE) VALUES ('no', 'no')");
}
connection.commit();
} catch (Exception e) {
System.err.println("Failure: UNKNOWN_TABLE");
connection.rollback();
} finally {
connection.close();
}
} catch (SQLException e2) {
e2.printStackTrace();
}
}
}
}
A couple of things with your code as you are trying to outsmart both Spring and Spring Boot. Instead of trying to do that, work with the frameworks instead of around them.
Ditch your #Configuration class and let Spring Boot do the configuration
Use TransactionTemplate instead of messing around with the (wrong!) Connection yourself.
Use the Spring TaskExecutor, configured by default, instead of manual access to a Executor.
Add this to your application.properties
spring.datasource.url=jdbc:oracle:thin:#url:port/schema
spring.datasource.username=xxxx
spring.datasource.password=xxxx
Instead of the messing around with the connection use a TransactionTemplate.
##SpringBootApplication
public class Application {
private static final String SQL = "INSERT INTO TB_MYTEST(MYKEY, MYVALUE) VALUES (?, ?)";
private static final String ERROR_SQL = "INSERT INTO UNKNOWN_TABLE(MYKEY, MYVALUE) VALUES (?, ?)";
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public CommandLineRunner testRunner(JdbcTemplate jdbc, TransactionTemplate tx, TaskExecutor tasks) {
return (args) -> {
jdbc.update(SQL, "start", "start");
IntStream.range(1, 6)
.forEach(id -> {
try {
tasks.execute(() -> tx.executeWithoutResult((s) -> {
jdbc.update(SQL, id, id);
if (id % 2 == 0) {
jdbc.update(ERROR_SQL, "no", "no");
}
}));
} catch (DataAccessException e) {
e.printStackTrace();
}
});
jdbc.update(SQL, "end", "end");
};
}
}
Something like the above would yield the result you want. Notice that you now use the JdbcTemplate, TransactionTemplate and TaskExecutor as provided by the frameworks.
After refer to #M. Deinum answer, I have change my code to below and it meet's my needs.
application.properties
spring.datasource.url=jdbc:oracle:thin:#ip:port/schema
spring.datasource.username=xxxx
spring.datasource.password=xxxx
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
Main Class
#SpringBootApplication
public class Application implements CommandLineRunner {
#Autowired
private TestService testService;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Override
public void run(String... args) throws Exception {
testService.test();
System.exit(0);
}
}
TestService
#Service
public class TestService {
#Autowired
private JdbcTemplate jdbcTemplate;
#Autowired
private PlatformTransactionManager transactionManager;
#Transactional(rollbackFor = Exception.class)
public void test() throws Exception {
jdbcTemplate.batchUpdate("INSERT INTO TB_MYTEST(MYKEY, MYVALUE) VALUES ('start', 'start')");
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 1; i <= 5; i++) {
executorService.submit(new TestRunner(i));
}
executorService.shutdown();
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
jdbcTemplate.batchUpdate("INSERT INTO TB_MYTEST(MYKEY, MYVALUE) VALUES ('end', 'end')");
}
private class TestRunner implements Runnable {
private Integer id;
public TestRunner(Integer id) {
this.id = id;
}
#Override
public void run() {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
#Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
String sqlString = String.format("INSERT INTO TB_MYTEST(MYKEY, MYVALUE) VALUES ('%d', '%d')", id, id);
jdbcTemplate.batchUpdate(sqlString);
if (id % 2 == 0) {
jdbcTemplate.batchUpdate("INSERT INTO UNKNOWN_TABLE(MYKEY, MYVALUE) VALUES ('no', 'no')");
}
}
});
}
}
}
With result 'start, 1, 3, 5, end'.
I have simple retryable method annotated with
#Retryable(label = "myLabel")
According to documentation, it should be
A unique label for statistics reporting
Is this label accessible inside RetryListener? How can i use it?
The label is available in the RetryContext.NAME attribute in the context.
#Component
class Foo {
private static final Logger log = LoggerFactory.getLogger(Foo.class);
#Retryable(label = "myLabel")
public void retriable() {
log.info("Here with label: " + RetrySynchronizationManager.getContext().getAttribute(RetryContext.NAME));
throw new RuntimeException("test");
}
#Recover
public void recover(Exception e) {
log.info("Recovered");
}
}
The context is available in the listener methods.
There's a new feature in 1.3.
1.3 is not released yet but there is a snapshot 1.3.0.BUILD-SNAPSHOT in the spring snapshots repo https://repo.spring.io/snapshot.
This also gives you access to the method invocation.
#Component
class MyRetryListener extends MethodInvocationRetryListenerSupport {
private static final Logger log = LoggerFactory.getLogger(MyRetryListener.class);
#Override
protected <T, E extends Throwable> boolean doOpen(RetryContext context,
MethodInvocationRetryCallback<T, E> callback) {
log.info("Invocation of method: " + callback.getInvocation().getMethod().toGenericString()
+ " with label: " + callback.getLabel());
return super.doOpen(context, callback);
}
}
#Component
class Foo {
private static final Logger log = LoggerFactory.getLogger(Foo.class);
#Retryable(label = "myLabel")
public void retriable() {
log.info("Here");
throw new RuntimeException("test");
}
#Recover
public void recover(Exception e) {
log.info("Recovered");
}
}
I have created an adapterImpl class that will retry a method with an objA but if it throws an exception(hardcoded to throw) it will call recover method - which will again call the method with objB.
My problem is - The #Recover method is not called. I am not sure what I am doing wrong here.
Spring version - 4.3.5.RELEASE
Spring retry - 1.2.1.RELEASE
My Config class -
#Configuration
#EnableRetry
public class ConfigClass {
#Bean
public ClassTest beanA(){
ClassTest obj = new ClassTest();
obj.setProp(5);
return obj;
}
#Bean
public ClassTest beanB(){
ClassTest obj = new ClassTest();
obj.setProp(10);
return obj;
}
#Bean("adapterImpl")
public AdapterInterfaceImpl adapter(){
AdapterInterfaceImpl obj = new AdapterInterfaceImpl();
return obj;
}
}
My AdapterInterfaceImpl class -
public class AdapterInterfaceImpl implements AdapterInterface{
#Autowired
#Qualifier("beanA")
private ClassTest objA;
#Autowired
#Qualifier("beanB")
private ClassTest objB;
public ClassTest getObjA() {
return objA;
}
public void setObjA(ClassTest objA) {
this.objA = objA;
}
public ClassTest getObjB() {
return objB;
}
public void setObjB(ClassTest objB) {
this.objB = objB;
}
#Retryable(maxAttempts = 3, include = Exception.class, backoff = #Backoff(delay = 2000))
public int getValue(int val) throws Exception{
System.out.println("obj A get Value");
return getValue(objA,val);
}
public int getValue(ClassTest obj, int val) throws Exception{
System.out.println("get Value");
if(obj==objA){
throw new Exception("This is msg");
}
return obj.methodA(val);
}
#Recover
public int getValue(Exception e, int val){
System.out.println("Recover get Value");
try{
return getValue(objB,val);
}catch(Exception e1){
return 0;
}
}
My ClassTest class -
public class ClassTest {
private int prop;
public int getProp() {
return prop;
}
public void setProp(int prop) {
this.prop = prop;
}
public int methodA(int x){
return x+prop;
}
}
My class with main method -
public class App
{
public static void main( String[] args )
{
AbstractApplicationContext context = new
AnnotationConfigApplicationContext(ConfigClass.class);
AdapterInterface adapter = (AdapterInterface)
context.getBean("adapterImpl");
try {
System.out.println(adapter.getValue(3));
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
My output is not showing any retry nor recovery -
A get Value
get Value
obj A get Value
get Value
obj A get Value
get Value
org.springframework.retry.ExhaustedRetryException: Cannot locate recovery method; nested exception is java.lang.Exception: This is msg
Spring Retry uses AOP, internal calls (from getValue(int) to getValue(ClassTest, int)) won't go through the proxy.
You have to put the #Retryable on the method that is called externally so that the proxy can intercept the call and apply the retry logic.
There is a similar issue reported in https://github.com/spring-projects/spring-retry/issues/75
So #EnableRetry(proxyTargetClass=true) works as it is now able to find the recovery method in the implementation class.
How do I write a unit test to verify async behavior using Spring 4 and annotations?
Since i'm used to Spring's (old) xml style), it took me some time to figure this out. So I thought I answer my own question to help others.
First the service that exposes an async download method:
#Service
public class DownloadService {
// note: placing this async method in its own dedicated bean was necessary
// to circumvent inner bean calls
#Async
public Future<String> startDownloading(final URL url) throws IOException {
return new AsyncResult<String>(getContentAsString(url));
}
private String getContentAsString(URL url) throws IOException {
try {
Thread.sleep(1000); // To demonstrate the effect of async
InputStream input = url.openStream();
return IOUtils.toString(input, StandardCharsets.UTF_8);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
Next the test:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration
public class DownloadServiceTest {
#Configuration
#EnableAsync
static class Config {
#Bean
public DownloadService downloadService() {
return new DownloadService();
}
}
#Autowired
private DownloadService service;
#Test
public void testIndex() throws Exception {
final URL url = new URL("http://spring.io/blog/2013/01/16/next-stop-spring-framework-4-0");
Future<String> content = service.startDownloading(url);
assertThat(false, equalTo(content.isDone()));
final String str = content.get();
assertThat(true, equalTo(content.isDone()));
assertThat(str, JUnitMatchers.containsString("<html"));
}
}
If you are using the same example in Java 8 you could also use the CompletableFuture class as follows:
#Service
public class DownloadService {
#Async
public CompletableFuture<String> startDownloading(final URL url) throws IOException {
CompletableFuture<Boolean> future = new CompletableFuture<>();
Executors.newCachedThreadPool().submit(() -> {
getContentAsString(url);
future.complete(true);
return null;
});
return future;
}
private String getContentAsString(URL url) throws IOException {
try {
Thread.sleep(1000); // To demonstrate the effect of async
InputStream input = url.openStream();
return IOUtils.toString(input, StandardCharsets.UTF_8);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
Now the test:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration
public class DownloadServiceTest {
#Configuration
#EnableAsync
static class Config {
#Bean
public DownloadService downloadService() {
return new DownloadService();
}
}
#Autowired
private DownloadService service;
#Test
public void testIndex() throws Exception {
final URL url = new URL("http://spring.io/blog/2013/01/16/next-stop-spring-framework-4-0");
CompletableFuture<Boolean> content = service.startDownloading(url);
content.thenRun(() -> {
assertThat(true, equalTo(content.isDone()));
assertThat(str, JUnitMatchers.containsString("<html"));
});
// wait for completion
content.get(10, TimeUnit.SECONDS);
}
}
Please that when the time-out is not specified, and anything goes wrong the test will go on "forever" until the CI or you shut it down.