How do I write a unit test to verify async behavior using Spring 4 and annotations? - spring

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.

Related

Spring-boot: Beans are null during unit/integration test

I am new to sprint-boot. I have a spring-boot application which is working fine in it's regular path. Now as I am trying to write unit/integration tests, I find that my beans are null.
I appreciate any help on understanding why are they null and how to fix it. It seems that it is not able to pick up properties from the yml at all.Please let me know if any more clarification is required.
To clarify the structure:
The main class:
#SpringBootApplication
#EnableConfigurationProperties(ApplicationConfiguration.class)
public class Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Application.class, args);
}
}
The properties file (src/main/java/resources/application.yml)
http:
url:
protocol: http
baseUrl: ${CONNECTOR_BASE_URL}
connectorListUrl : connectors
The configuration class that is using the above properties (ApplicationConfiguration.java) is :
#ConfigurationProperties(prefix = "http.url")
#Validated
#Data
public class ApplicationConfiguration {
private String protocol;
private String baseUrl;
private String connectorListUrl;
}
Now, the simplified version of the class(ContinuousMonitorServiceTask.java that I am trying to write my test on, looks like :
#Component
#Slf4j
public class ContinuousMonitorServiceTask extends TimerTask {
#Autowired MonitorHttpClient httpClient;
#Autowired ApplicationConfiguration config;
#PostConstruct
public void setUp() {
connectorListUrl =
config.getProtocol() + "://" + config.getBaseUrl() + "/" + config.getConnectorListUrl();
connectorListHeaderParams.clear();
connectorListHeaderParams.put("Accept", "application/json");
connectorListHeaderParams.put("Content-Type", "application/json");
connectorListGetRequest = new HttpGet(connectorListUrl);
httpClient.setHeader(connectorListGetRequest, connectorListHeaderParams);
}
public void fetchList() {
try {
response = httpClient.callApi("Get Connector List", connectorListGetRequest);
log.info(response.toString());
connectorListResponseHandler(response);
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
The above code is working fine when I am executing.
Now when I am writing test, I need to mock api calls and hence, I have used MOCK-SERVER and my testSimple1 test has passed which is a simple test to see if the mock server can start and return expected response. However, while debugging simpleTest2, I am seeing
monitorTask is null
appConfig is null
monitorTask is null
Although, I have src/test/resources/application.yml as:
http:
url:
protocol: http
baseUrl: 127.0.0.1:8080
connectorListUrl : connectors
My guess is that appConfig is not able to pick up the properties from application.yml during test and hence everything is null.However, I am not 100% sure about what is happening in real time.
Here is how my test class looks like (Kind of dirty code, but I am putting it in it's current state to show what I have tried so far):
//#RunWith(MockitoJUnitRunner.class)
//#TestPropertySource(locations="classpath:application.yml")
//#RunWith(SpringJUnit4ClassRunner.class)
//#SpringApplicationConfiguration(ApplicationConfiguration.class)
#RunWith(SpringRunner.class)
#SpringBootTest(classes = ApplicationConfiguration.class)
//#EnableConfigurationProperties(ApplicationConfiguration.class)
public class ContinousMonitorTest {
private MockMvc mockMvc;
#Mock private MonitorHttpClient httpClient;
#Mock private ApplicationConfiguration appConfig;
#InjectMocks
//#MockBean
//#Autowired
private ContinuousMonitorServiceTask monitorTask;
TestRestTemplate restTemplate = new TestRestTemplate();
HttpHeaders headers = new HttpHeaders();
private static ClientAndServer mockServer;
#BeforeClass
public static void startServer() {
mockServer = startClientAndServer(8080);
}
#AfterClass
public static void stopServer() {
mockServer.stop();
}
private void createExpectationForInvalidAuth() {
new MockServerClient("127.0.0.1", 8080)
.when(
request()
.withMethod("GET")
.withPath("/validate")
.withHeader("\"Content-type\", \"application/json\""),
//.withBody(exact("{username: 'foo', password: 'bar'}")),
exactly(1))
.respond(
response()
.withStatusCode(401)
.withHeaders(
new Header("Content-Type", "application/json; charset=utf-8"),
new Header("Cache-Control", "public, max-age=86400"))
.withBody("{ message: 'incorrect username and password combination' }")
.withDelay(TimeUnit.SECONDS,1)
);
}
private GenericResponse hitTheServerWithGETRequest() {
String url = "http://127.0.0.1:8080/validate";
HttpClient client = HttpClientBuilder.create().build();
HttpGet post = new HttpGet(url);
post.setHeader("Content-type", "application/json");
GenericResponse response=null;
try {
StringEntity stringEntity = new StringEntity("{username: 'foo', password: 'bar'}");
post.getRequestLine();
// post.setEntity(stringEntity);
response=client.execute(post, new GenericResponseHandler());
} catch (Exception e) {
throw new RuntimeException(e);
}
return response;
}
#Test
public void testSimple1() throws Exception{
createExpectationForInvalidAuth();
GenericResponse response = hitTheServerWithGETRequest();
System.out.println("response customed : " + response.getResponse());
assertEquals(401, response.getStatusCd());
monitorTask.fetchConnectorList();
}
#Test
public void testSimple2() throws Exception{
monitorTask.fetchConnectorList();
}
as #second suggested above, I made a change in the testSimple2 test to look like and that resolved the above mentioned problem.
#Test
public void testSimple2() throws Exception{
mockMvc = MockMvcBuilders.standaloneSetup(monitorTask).build();
Mockito.when(appConfig.getProtocol()).thenReturn("http");
Mockito.when(appConfig.getBaseUrl()).thenReturn("127.0.0.1:8080");
Mockito.when(appConfig.getConnectorListUrl()).thenReturn("validate");
Mockito.when(httpClient.callApi(Mockito.any(), Mockito.any())).thenCallRealMethod();
monitorTask.setUp();
monitorTask.fetchConnectorList();
}
Alternatively, could have done:
#Before
public void init()
{
MockitoAnnotations.initMocks(this);
}

Bean not getting overridden in Spring boot

I am trying to write and test an application that used spring-cloud with azure functions following this tutorial.
https://github.com/markusgulden/aws-tutorials/tree/master/spring-cloud-function/spring-cloud-function-azure/src/main/java/de/margul/awstutorials/springcloudfunction/azure
I am tryign to write a testcase and override the bean.
Here is the application class having function and handler Bean function.
#SpringBootApplication
#ComponentScan(basePackages = { "com.package" })
public class DataFunctions extends AzureSpringBootRequestHandler<GenericMessage<Optional<String>>, Data> {
#FunctionName("addData")
public HttpResponseMessage addDataRun(
#HttpTrigger(name = "add", methods = {
HttpMethod.POST }, authLevel = AuthorizationLevel.FUNCTION) HttpRequestMessage<Optional<String>> request,
final ExecutionContext context) throws JsonParseException, JsonMappingException, IOException {
context.getLogger().info("Java HTTP trigger processed a POST request.");
try {
handleRequest(new GenericMessage<Optional<String>>(request.getBody()), context);
} catch (ServiceException ex) {
ErrorMessage em = new ErrorMessage();
return request.createResponseBuilder(handleException(ex, em)).body(em).build();
}
return request.createResponseBuilder(HttpStatus.CREATED).build();
}
#Autowired
MyService mService;
#Bean
public Consumer<GenericMessage<Optional<String>>> addData() {
ObjectMapper mapper = new ObjectMapper();
return req -> {
SomeModel fp = null;
try {
fp = mapper.readValue(req.getPayload().get(), SomeModel.class);
} catch (Exception e) {
throw new ServiceException(e);
}
mService.addData(fp);
};
}
}
I want to test by overriding the above bean.
Cosmosdb spring configuration
#Configuration
#EnableDocumentDbRepositories
public class CosmosDBConfig extends AbstractDocumentDbConfiguration {
#Value("${cosmosdb.collection.endpoint}")
private String uri;
#Value("${cosmosdb.collection.key}")
private String key;
#Value("${cosmosdb.collection.dbname}")
private String dbName;
#Value("${cosmosdb.connect.directly}")
private Boolean connectDirectly;
#Override
public DocumentDBConfig getConfig() {
ConnectionPolicy cp = ConnectionPolicy.GetDefault();
if (connectDirectly) {
cp.setConnectionMode(ConnectionMode.DirectHttps);
} else {
cp.setConnectionMode(ConnectionMode.Gateway);
}
return DocumentDBConfig.builder(uri, key, dbName).connectionPolicy(cp).build();
}
}
Here is the configuration
#TestConfiguration
#PropertySource(value = "classpath:application.properties", encoding = "UTF-8")
#Profile("test")
#Import({DataFunctions.class})
public class TestConfig {
#Bean(name="addData")
#Primary
public Consumer<GenericMessage<Optional<String>>> addData() {
return req -> {
System.out.println("data mock");
};
}
#Bean
#Primary
public DocumentDBConfig getConfig() {
return Mockito.mock(DocumentDBConfig.class);
}
}
Finally the test class
#RunWith(SpringRunner.class)
//#SpringBootTest //Enabling this gives initialization error.
#ActiveProfiles("test")
public class TempTest {
#InjectMocks
DataFunctions func;
#Mock
MyService mService;
#Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
private Optional<String> createRequestString(final String res) throws IOException {
InputStream iStream = TempTest.class.getResourceAsStream(res);
String charset="UTF-8";
try (BufferedReader br = new BufferedReader(new InputStreamReader(iStream, charset))) {
return Optional.of(br.lines().collect(Collectors.joining(System.lineSeparator())));
}
}
#Test
public void testHttpPostTriggerJava() throws Exception {
#SuppressWarnings("unchecked")
final HttpRequestMessage<Optional<String>> req = mock(HttpRequestMessage.class);
final Optional<String> queryBody = createRequestString("/test-data.json");
doNothing().when(mService).addData(Mockito.any(SomeModel.class));
doReturn(queryBody).when(req).getBody();
doAnswer(new Answer<HttpResponseMessage.Builder>() {
#Override
public HttpResponseMessage.Builder answer(InvocationOnMock invocation) {
HttpStatus status = (HttpStatus) invocation.getArguments()[0];
return new HttpResponseMessageMock.HttpResponseMessageBuilderMock().status(status);
}
}).when(req).createResponseBuilder(any(HttpStatus.class));
final ExecutionContext context = mock(ExecutionContext.class);
doReturn(Logger.getGlobal()).when(context).getLogger();
doReturn("addData").when(context).getFunctionName();
// Invoke
final HttpResponseMessage ret = func.addDataRun(req, context);
// Verify
assertEquals(ret.getStatus(), HttpStatus.CREATED);
}
}
For this case instead of test configuration addData the actual bean is called from DataFunctions class. Also the database connection is also created when it should use the mocked bean from my test configuration. Can somebody please point out what is wrong in my test configuration?
I was able to resolve the first part of cosmos db config loading by marking it with
#Configuration
#EnableDocumentDbRepositories
#Profile("!test")
public class CosmosDBConfig extends AbstractDocumentDbConfiguration {
...
}
Also had to mark the repository bean as optional in the service.
public class MyService {
#Autowired(required = false)
private MyRepository myRepo;
}
Didn't use any spring boot configuration other than this.
#ActiveProfiles("test")
public class FunctionTest {
...
}
For the second part of providing mock version of Mock handlers, I simply made the test config file as spring application as below.
#SpringBootApplication
#ComponentScan(basePackages = { "com.boeing.da.helix.utm.traffic" })
#Profile("test")
public class TestConfiguration {
public static void main(final String[] args) {
SpringApplication.run(TestConfiguration.class, args);
}
#Bean(name="addData")
#Primary
public Consumer<GenericMessage<Optional<String>>> addData() {
return req -> {
System.out.println("data mock");
};
}
}
and made use of this constructor from azure functions library in spring cloud in my constructor
public class AppFunctions
extends AzureSpringBootRequestHandler<GenericMessage<Optional<String>>, List<Data>> {
public AppFunctions(Class<?> configurationClass) {
super(configurationClass);
}
}
public AzureSpringBootRequestHandler(Class<?> configurationClass) {
super(configurationClass);
}
Hope it helps someone.

Loading a custom ApplicationContextInitializer in AWS Lambda Spring boot

How to loada custom ApplicationContextInitializer to in spring boot AWS Lambda?
I have an aws lambda application using spring boot, I would like to write an ApplicationContextInitializer for decrypting database passwords. I have the following code that works while running it as a spring boot application locally, but when I deploy it to the AWS console as a lambda it doesn't work.
Here is my code
1. applications.properties
spring.datasource.url=url
spring.datasource.username=testuser
CIPHER.spring.datasource.password=encryptedpassword
The following code is the ApplicationContextInitializer, assuming password is Base64 encoded for testing only (In the actual case it will be encrypted by AWM KMS). The idea here is if the key is starting with 'CIPHER.' (as in CIPHER.spring.datasource.password)I assume it's value needs to be decrypted and another key value pair with actual, key (here spring.datasource.password) and its decrypted value will be added at context initialization.
will be like spring.datasource.password=decrypted password
#Component
public class DecryptedPropertyContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private static final String CIPHER = "CIPHER.";
#Override
public void initialize(ConfigurableApplicationContext applicationContext) {
ConfigurableEnvironment environment = applicationContext.getEnvironment();
for (PropertySource<?> propertySource : environment.getPropertySources()) {
Map<String, Object> propertyOverrides = new LinkedHashMap<>();
decodePasswords(propertySource, propertyOverrides);
if (!propertyOverrides.isEmpty()) {
PropertySource<?> decodedProperties = new MapPropertySource("decoded "+ propertySource.getName(), propertyOverrides);
environment.getPropertySources().addBefore(propertySource.getName(), decodedProperties);
}
}
}
private void decodePasswords(PropertySource<?> source, Map<String, Object> propertyOverrides) {
if (source instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerablePropertySource = (EnumerablePropertySource<?>) source;
for (String key : enumerablePropertySource.getPropertyNames()) {
Object rawValue = source.getProperty(key);
if (rawValue instanceof String && key.startsWith(CIPHER)) {
String cipherRemovedKey = key.substring(CIPHER.length());
String decodedValue = decode((String) rawValue);
propertyOverrides.put(cipherRemovedKey, decodedValue);
}
}
}
}
public String decode(String encodedString) {
byte[] valueDecoded = org.apache.commons.codec.binary.Base64.decodeBase64(encodedString);
return new String(valueDecoded);
}
Here is the Spring boot initializer
#SpringBootApplication
#ComponentScan(basePackages = "com.amazonaws.serverless.sample.springboot.controller")
public class Application extends SpringBootServletInitializer {
#Bean
public HandlerMapping handlerMapping() {
return new RequestMappingHandlerMapping();
}
#Bean
public HandlerAdapter handlerAdapter() {
return new RequestMappingHandlerAdapter();
}
#Bean
public HandlerExceptionResolver handlerExceptionResolver() {
return new HandlerExceptionResolver() {
#Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
return null;
}
};
}
//loading the initializer here
public static void main(String[] args) {
SpringApplication application=new SpringApplication(Application.class);
application.addInitializers(new DecryptedPropertyContextInitializer());
application.run(args);
}
This is working when run as a spring boot appliaction, But when it deployed as a lambda into AWS the main() method in my SpringBootServletInitializer will never be called by lambda. Here is my Lambda handler.
public class StreamLambdaHandler implements RequestStreamHandler {
private static Logger LOGGER = LoggerFactory.getLogger(StreamLambdaHandler.class);
private static SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
static {
try {
handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class);
handler.onStartup(servletContext -> {
FilterRegistration.Dynamic registration = servletContext.addFilter("CognitoIdentityFilter", CognitoIdentityFilter.class);
registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
});
} catch (ContainerInitializationException e) {
e.printStackTrace();
throw new RuntimeException("Could not initialize Spring Boot application", e);
}
}
#Override
public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
throws IOException {
handler.proxyStream(inputStream, outputStream, context);
outputStream.close();
}
}
What change is to be made in the code to load the ApplicationContextInitializer by Lambda? Any help will be highly appreciated.
I was able to nail it in the following way.
First changed the property value with place holder with a prefix, where the prefix denotes the values need to be decrypted, ex.
spring.datasource.password=${MY_PREFIX_placeHolder}
aws lambda environment variable name should match to the placeholder
('MY_PREFIX_placeHolder') and it value is encrypted using AWS KMS (This sample is base64 decoding).
create an ApplicationContextInitializer which will decrypt the property value
public class DecryptedPropertyContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private static final String CIPHER = "MY_PREFIX_";
#Override
public void initialize(ConfigurableApplicationContext applicationContext) {
ConfigurableEnvironment environment = applicationContext.getEnvironment();
for (PropertySource<?> propertySource : environment.getPropertySources()) {
Map<String, Object> propertyOverrides = new LinkedHashMap<>();
decodePasswords(propertySource, propertyOverrides);
if (!propertyOverrides.isEmpty()) {
PropertySource<?> decodedProperties = new MapPropertySource("decoded "+ propertySource.getName(), propertyOverrides);
environment.getPropertySources().addBefore(propertySource.getName(), decodedProperties);
}
}
}
private void decodePasswords(PropertySource<?> source, Map<String, Object> propertyOverrides) {
if (source instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerablePropertySource = (EnumerablePropertySource<?>) source;
for (String key : enumerablePropertySource.getPropertyNames()) {
Object rawValue = source.getProperty(key);
if (rawValue instanceof String && key.startsWith(CIPHER)) {
String decodedValue = decode((String) rawValue);
propertyOverrides.put(key, decodedValue);
}
}
}
}
public String decode(String encodedString) {
byte[] valueDecoded = org.apache.commons.codec.binary.Base64.decodeBase64(encodedString);
return new String(valueDecoded);
}
}
The above code will decrypt all the values with prefix MY_PREFIX_ and add them at the top of the property source.
As the spring boot is deployed into aws lambda, lambda will not invoke the main() function, so if the ApplicationContextInitializer is initialized in main() it is not going to work. In order to make it work need to override createSpringApplicationBuilder() method of SpringBootServletInitializer, so SpringBootServletInitializer will be like
#SpringBootApplication
#ComponentScan(basePackages = "com.amazonaws.serverless.sample.springboot.controller")
public class Application extends SpringBootServletInitializer {
#Bean
public HandlerMapping handlerMapping() {
return new RequestMappingHandlerMapping();
}
#Bean
public HandlerAdapter handlerAdapter() {
return new RequestMappingHandlerAdapter();
}
#Bean
public HandlerExceptionResolver handlerExceptionResolver() {
return new HandlerExceptionResolver() {
#Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
return null;
}
};
}
#Override
protected SpringApplicationBuilder createSpringApplicationBuilder() {
SpringApplicationBuilder builder = new SpringApplicationBuilder();
builder.initializers(new DecryptedPropertyContextInitializer());
return builder;
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
No need to make any changes for the lambdahandler.

Spring `#Autowire` field is `null` eventhough it works fine in other classes

Spring #Autowire field is null even though it works fine in other classes successfully.
public class SendRunner implements Runnable {
private String senderAddress;
#Autowired
private SubscriberService subscriberService;
public SendRunner(String senderAddress) {
this.senderAddress = senderAddress;
}
#Override
public void run() {
sendRequest();
}
private void sendRequest() {
try {
HashMap<String, String> dataMap = new HashMap<>();
dataMap.put("subscriberId", senderAddress);
HttpEntity<?> entity = new HttpEntity<Object>(dataMap, httpHeaders);
Subscriber subscriber = subscriberService.getSubscriberByMsisdn(senderAddress);
} catch (Exception e) {
logger.error("Error occurred while trying to send api request", e);
}
}
Also this class is managed as a bean in the dispatcher servlet :
<bean id="SendRunner" class="sms.dating.messenger.connector.SendRunner">
</bean>
In here i'm getting a null pointer exception for subscriberService. What would be the possible reason for this? Thanks in advance.
Can you please try with below code snippet
#Configuration
public class Someclass{
#Autowired
private SubscriberService subscriberService;
Thread subscriberThread = new Thread() {
#Override
public void run() {
try {
HashMap<String, String> dataMap = new HashMap<>();
dataMap.put("subscriberId", senderAddress);
HttpEntity<?> entity = new HttpEntity<Object>(dataMap, httpHeaders);
Subscriber subscriber = subscriberService.getSubscriberByMsisdn(senderAddress);
} catch (Exception e) {
logger.error("Error occurred while trying to send api request", e);
}
}
};
}
Can you please annotate your SendRunner class with #Component or #Service and include the SendRunner package in componentscanpackage
Your bean not in Spring Managed context, below can be the reasons.
Package sms.dating.messenger.connector not in Component scan.
You are moving out of the Spring context by creating an object with new (see below),
this way you will not get the autowired fields.
SendRunner sendRunner = new SendRunner () ,
sendRunner.sendRequest();
Just check how I implement. Hope this will help.
#RestController
public class RestRequest {
#Autowired
SendRunner sendRunner;
#RequestMapping("/api")
public void Uri() {
sendRunner.start();
}
}
SendRunner class
#Service
public class SendRunner extends Thread{
#Autowired
private SubscriberService subscriberService;
#Override
public void run() {
SendRequest();
}
private void SendRequest() {
System.out.println("Object is " + subscriberService);
String senderAddress = "address";
subscriberService.getSubscriberByMsisdn(senderAddress);
}
}
Below are the logs printed when I hit the REST api.
Object is com.example.demo.SubscriberService#40f33492

Logging requests and responses in Spring

I'm trying to implement logging system in a Spring boot application. There are requests coming into the system which have one or more responses.
Requests and responses must be logged into the database in a separate thread, not in the worker thread.
This is my idea.
tables in mysql - "request" with required columns, and "response" with request_id as foreign key
relation between resquest and response - one to many.
A separate thread in LogService is started in #PostContruct to save the data in the DB.
I'm sure there are better solutions to this problem. Please guide with some suggestions.
#Service
public class LogServiceImpl implements LogService {
private final BlockingQueue<Object> logQueue = new LinkedBlockingQueue<>();
private volatile boolean done;
// repositories
#Autowired
private RequestRepository requestRepository;
#Autowired
private ResponseRepository responseRepository;
#Async
#Override
public void log(Object obj) {
try {
logQueue.put(obj);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
#PostContruct
private saveToDb(){
new Thread(() -> {
while(!done){
String object = logQueue.poll(5, TimeUnit.SECONDS)
if(object != null){
if(object instanceof Request){
requestRepository.save((Request)object);
}
if(object instanceof Response){
responseRepository.save((Response)object);
}
}
}
}).start();
}
public void stop() {
done = true;
}
}
class Request{
.....
}
class Response{
......
}
#Service
public class SomeService1 {
#Autowired
private LogService logService;
public void someMeth1(Request request) {
....
logService.log(request);
}
}
#Service
public class SomeService2 {
#Autowired
private LogService logService;
public void someMeth2(Response response) {
....
logService.log(response);
}
}

Resources