My junit test is not #Autowiring. What am I missing - spring-boot

I am trying to unit test my Spring Boot project in Eclipse. The problem I am having is that my #Autowire s are being ignored.
#SpringBootTest
public class ValidateRepositoryTest {
private static final String CREATE_TBLVALIDATE_SQL_SCRIPT = "scripts/create/validate.sql";
private static final String DROP_TBLVALIDATE_SQL_SCRIPT = "scripts/drop/validate.sql";
private static final Logger logger = Logger.getLogger(ValidateRepositoryTest.class);
#Autowired
private JdbcTemplate jdbc;
#Before
public void before() throws SQLException {
if (jdbc == null) {
logger.fatal("jdbc == null in ValidateRepositoryTest.before()");
return;
}
ScriptUtils.executeSqlScript(jdbc.getDataSource().getConnection(), new ClassPathResource(CREATE_TBLVALIDATE_SQL_SCRIPT));
}
#After
public void after() throws SQLException {
if (jdbc == null) {
logger.fatal("jdbc == null in ValidateRepositoryTest.before()");
return;
}
ScriptUtils.executeSqlScript(jdbc.getDataSource().getConnection(), new ClassPathResource(DROP_TBLVALIDATE_SQL_SCRIPT));
}
#Autowired
ValidateRepository validateRepository;
#Test
public void testFindByKeyCode() {
if (jdbc == null) {
logger.fatal("validateRepository == null in ValidateRepositoryTest.testFindByKeyCode()");
return;
}
String documentTypeKeyCode = Validate.DOCUMENT_TYPE_CLAIMS_APPROVAL;
String sendMethodKeyCode = Validate.DOCUMENT_SEND_METHOD_EMAIL;
Validate validate = validateRepository.findByKeyCode(documentTypeKeyCode);
assertEquals("Shortage Claims Approval POD", validate.getDescription());
}
}
The output.
[INFO] Running com.kable.newsstand.batch.shortage_claim_auto_accept.entities.validate.test.ValidateRepositoryTest
09:40:51.029 [main] ERROR com.kable.newsstand.batch.shortage_claim_auto_accept.entities.validate.test.ValidateRepositoryTest - jdbc == null in ValidateRepositoryTest.before()
09:40:51.036 [main] ERROR com.kable.newsstand.batch.shortage_claim_auto_accept.entities.validate.test.ValidateRepositoryTest - validateRepository == null in ValidateRepositoryTest.testFindByKeyCode()
09:40:51.036 [main] ERROR com.kable.newsstand.batch.shortage_claim_auto_accept.entities.validate.test.ValidateRepositoryTest - jdbc == null in ValidateRepositoryTest.after()
I think it might have something to do with not having #RunWith(SpringRunner.class) at the top, but if I include this, it tries to run my main application, which throws exceptions because instance variables aren't populated from the environment:
#SpringBootApplication
public class ShortageClaimAutoAccept implements CommandLineRunner {
private static final Logger logger = Logger.getLogger(ShortageClaimAutoAccept.class);
private static String cognosUser;
private static String cognosPassword;
private static String smtpHost;
private static String ftpServer;
private static String ftpUserName;
private static String ftpPassword;
private static String ftpPath;
private static Mailer mailer;
private static FtpRelativePathUsage ftp;
#Autowired
JdbcTemplate jdbcTemplate;
#Autowired
ClaimRepository claimRepository;
#Autowired
ClaimDetailRepository claimDetailRepository;
#Autowired
DocumentControlRepository documentControlRepository;
#Autowired
DocumentReportRepository documentReportRepository;
#Autowired
ValidateRepository validateRepository;
private void startClaimAutoAcceptApp() {
if (smtpHost == null) {
throw new IllegalStateException("smtp host is null");
}
if (cognosUser == null) {
throw new IllegalStateException("cognos user is null");
}
if (cognosPassword == null) {
throw new IllegalStateException("cognos password is null");
}
if (ftpServer == null) {
throw new IllegalStateException("ftp host is null");
}
if (ftpUserName == null) {
throw new IllegalStateException("ftp user is null");
}
if (ftpPassword == null) {
throw new IllegalStateException("ftp password is null");
}
if (ftpPath == null) {
throw new IllegalStateException("ftp server base path is null");
}
acceptClaimDetailsAndCloseClaims();
emailPODClaims();
}
public static void main(String[] args) {
try {
final Properties props = ApplicationPropertiesProvider.getProperties();
smtpHost = props.getProperty("SYS_SMTP_HOST");
cognosUser = props.getProperty("REPORT_RUNNER_USER");
cognosPassword = props.getProperty("REPORT_RUNNER_PWD");
ftpServer = props.getProperty("FTP_I_CLAIMSPOD_HOST");
ftpUserName = props.getProperty("FTP_I_CLAIMSPOD_USRID");
ftpPassword = props.getProperty("FTP_I_CLAIMSPOD_PWD");
ftpPath = props.getProperty("FTP_I_CLAIMS_BASE_PATH");
mailer = new Mailer(smtpHost, "");
FtpSite ftpSite = new FtpSite(ftpServer, ftpUserName, ftpPassword, ftpPath);
ftp = new FtpRelativePathUsage(ftpSite);
SpringApplication.run(ShortageClaimAutoAccept.class, args);
} catch (IllegalStateException ex) {
logger.fatal(ex);
} catch (Exception ex) {
logger.fatal("Uncaught exception in the process: \n", ex);
}
}
#Override
public void run(String... arg0) throws Exception {
startClaimAutoAcceptApp();
}
}
I'm not sure why it tries to run this, when all I am doing is testing.

You are missing two things here:
A runner
#RunWith(SpringRunner.class)
The component classes to use for loading an ApplicationContext. Can also
be specified using #ContextConfiguration(classes=...). If no
explicit classes are defined the test will look for nested Configuration
classes, before falling back to a SpringBootConfiguration search.
#SpringBootTest(classes = ShortageClaimAutoAccept.class)

Related

Show a static HTML page while the Spring context initializes

Spring Boot 2.3.7, Embedded Jetty, JSP and WAR packaging. I want to show my some static HTML page while spring context initializes. It should be visible when application starts and before spring context refreshed. I tried to use this manual as example https://www.nurkiewicz.com/2015/09/displaying-progress-of-spring.html but this doesn't work.
I need to start embedded jetty directly when jetty is initialized. But spring boot starts embedded jetty only when context refreshed.
How should I do this?
I created a Jetty warpper warmup class:
public final class WarmupServer {
private final String contextPath;
private final String displayName;
private final DefaultApplicationArguments arguments;
private final String[] welcomeFiles;
private final Resource baseResource;
private Server server;
public WarmupServer(String contextPath,
String displayName,
Resource baseResource,
String[] welcomeFiles,
String... runArgs) {
this.contextPath = StringUtils.defaultIfBlank(contextPath, "/");
this.displayName = StringUtils.defaultIfBlank(displayName, "Warmup");
this.baseResource = ObjectUtils.defaultIfNull(baseResource, Resource.newClassPathResource("/static"));
this.welcomeFiles = ArrayUtils.isEmpty(welcomeFiles) ? new String[]{"html/warmup.html"} : welcomeFiles;
this.arguments = new DefaultApplicationArguments(ArrayUtils.nullToEmpty(runArgs));
}
public Server start() throws Exception {
if (server != null && server.isStarted()) {
throw new IllegalStateException("Server already started");
}
server = new Server();
server.setHandler(buildServletHandler());
final String configPath = parseArg(OPT_CONFIG);
if (StringUtils.isBlank(configPath)) {
throw new RuntimeException(OPT_CONFIG + " argument is not set");
}
final Config config = ConfigUtils.parseFile(new File(configPath), DEFAULT_CONFIG_FILE_NAME);
configureHttpConnector(config);
configureHttpsConnector(config);
server.start();
return server;
}
public void registerWarmupServerStopLifecycle(ConfigurableApplicationContext context) {
context.getBeanFactory()
.registerSingleton(WarmupStopLifecycle.class.getSimpleName(), new WarmupStopLifecycle(server));
}
private ServletContextHandler buildServletHandler() {
final ServletContextHandler handler = new ServletContextHandler(ServletContextHandler.SESSIONS);
handler.addServlet(DefaultServlet.class, "/");
handler.setDisplayName(displayName);
handler.setContextPath(contextPath);
handler.setWelcomeFiles(welcomeFiles);
handler.setBaseResource(baseResource);
return handler;
}
private void configureHttpConnector(Config config) {
final int httpPort = NumberUtils.toInt(parseArg(OPT_HTTP_PORT), config.getInt(OPT_HTTP_PORT));
final ServerConnector connector = new ServerConnector(server);
connector.setPort(httpPort);
server.addConnector(connector);
}
private void configureHttpsConnector(Config config) {
final int httpsPort = NumberUtils.toInt(
parseArg(OPT_HTTPS_PORT), config.getInt(OPT_HTTPS_PORT));
final String keyStorePath = StringUtils.defaultIfBlank(
parseArg(OPT_KEYSTORE_FILE), config.getString(OPT_KEYSTORE_FILE));
final boolean sslEnabled = StringUtils.isNotBlank(keyStorePath)
&& Files.isReadable(Paths.get(keyStorePath));
if (sslEnabled) {
final HttpConfiguration configuration = new HttpConfiguration();
configuration.setSecurePort(httpsPort);
configuration.setSecureScheme(HTTPS_SCHEME);
final ServerConnector httpsConnector = new HttpsConnector()
.createConnector(server, configuration, config.getConfig(JETTY_HTTPS_CONFIG), httpsPort);
server.addConnector(httpsConnector);
}
}
private String parseArg(String optionName) {
final List<String> values = arguments.getOptionValues(optionName);
return CollectionUtils.isEmpty(values) ? StringUtils.EMPTY : values.get(0);
}
public static WarmupServer start(String contextPath,
String displayName,
Resource baseResource,
String[] welcomeFiles,
String... runArgs) throws Exception {
final WarmupServer server = new WarmupServer(contextPath, displayName, baseResource, welcomeFiles, runArgs);
server.start();
return server;
}
}
This wrapper parses command line arguments and creates a Jetty handler and HTTP and (or) HTTPS connectors by using provided command line arguments.
And the simple Spring's Lifecycle implementation class:
#RequiredArgsConstructor
class WarmupStopLifecycle implements SmartLifecycle {
private static final Logger logger = LogManager.getFormatterLogger();
private final Server warmupServer;
private volatile boolean isRunning;
#Override
public void start() {
try {
warmupServer.stop();
isRunning = true;
} catch (Exception e) {
logger.error("Failed to stop warmup server", e);
throw new RuntimeException("Failed to stop warmup server", e);
}
}
#Override
public void stop() {
}
#Override
public boolean isRunning() {
return isRunning;
}
/**
* Returns phase of this lifecycle.
* A phase MUST be before the Spring web server starts.
* See {#code org.springframework.boot.web.servlet.context.WebServerStartStopLifecycle} phase.
*/
#Override
public int getPhase() {
return Integer.MAX_VALUE - 2;
}
}
So usage of this:
#SpringBootApplication
public class SpringApplication {
public static void main(String[] args) throws Exception {
final WarmupServer warmupServer = WarmupServer.start(
"/my_context_path", "My Warmup server handler", args);
new SpringApplicationBuilder()
.sources(SpringApplication.class)
.initializers(warmupServer::registerWarmupServerStopLifecycle)
.run(args);
}
}
WarmupServer starts immediately after the application runs and will be stopped before starting the Spring's web server.

Update Question: How to Mock MapToJson Class

this is an update to my previous question. I have a Utility class which I already manage to cover with my unit test except when the condition returns false.
Here is my class:
#Component
public class Utils {
#Autowired
private ObjectMapper mapper = new ObjectMapper();
#Autowired
private LoggingService loggingService;
public <E> String mapToJsonString(E object) {
try {
if (object == null) {
throw new IOException(ErrorMessage.ERROR_PROCESSING_JSON_NULL);
}else {
return mapper.enable(SerializationFeature.INDENT_OUTPUT).writeValueAsString(object); //NullPointer Here
}
} catch (IOException e) {
loggingService.logError(this.getClass().getName(), "1", ErrorMessage.ERROR_MAPPING_TO_JSONSTRING, e);
return "";
}
}
}
and here is my unit test
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = ATMMonitoringApplication.class, initializers = ConfigFileApplicationContextInitializer.class)
public class ObjectToJsonStringTest {
#SpyBean
private ATM atm;
#Autowired
private Utils utils;
#MockBean
private ObjectMapper mapper;
#MockBean
private LoggingService loggingService;
#Before
public void setUp() {
MockitoAnnotations.initMocks(this);
myModelClass = new MyModelclass();
myModelClass.setStatus("U");
myModelClass.setTermCode("001");
myModelClass.setLocation("BGC");
}
#Test
public void testObjectToJson() throws JsonProcessingException {
String output = utils.mapToJsonStringmyModelClass
assertNotNull(output);
}
#Test
public void testObjectToJsonNull() throws JsonProcessingException {
String output = utils.mapToJsonString(null);
assertNull(output);
}
#Test
public void testJsonParsingException() {
myModelClass = new MyModelclass();
myModelClass = null;
String output = utils.mapToJsonString(myModelClass);
Mockito.when(loggingService.logError(this.getClass().getName(), "1", ErrorMessage.ERROR_MAPPING_TO_JSONSTRING, new Exception()))
.thenReturn("");
assertThat(output).isEmpty();
}
}
Stack trace says that i have a null pointer on this line of code:
return mapper.enable(SerializationFeature.INDENT_OUTPUT).writeValueAsString(object);
Please help me on this. Thanks
Solved it. I just change this
#Mock
private ObjectMapper mapper;
From #MockBean and it covered the whole class.

Spring Cloud Canary Deployment

I have a spring cloud micro service with Zuul running on docker.
Requirement:
I want to create canary deployment with specific requirement as we will have x clients and I want to canary test with y specific clients (using email or username).
Can I configure the gateway to route requests to the new version of the micro-service for these y clients?
So you can do that via configuration or dynamic routing but i think first idom is not good for generic part every client you have to define it again and again but second one is more good
#Component
public class PostFilter extends ZuulFilter {
private static final String REQUEST_PATH = "/special-customer-product-request-url";
private static final String TARGET_SERVICE = "special-customer-service";
private static final String HTTP_METHOD = "POST or GET";
private final DiscoveryClient discoveryClient;
public PostOrdersFilter(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
#Override
public String filterType() {
return "route";
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String method = request.getMethod();
String requestURI = request.getRequestURI();
return HTTP_METHOD.equalsIgnoreCase(method) && requestURI.startsWith(REQUEST_PATH);
}
#Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
List<ServiceInstance> instances = discoveryClient.getInstances(TARGET_SERVICE);
try {
if (instances != null && instances.size() > 0) {
context.setRouteHost(instances.get(0).getUri().toURL());
} else {
throw new IllegalStateException("Target service instance not found!");
}
} catch (Exception e) {
throw new IllegalArgumentException("Couldn't get service URL!", e);
}
return null;
}
}

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 Batch MultiResourceItemReader re-reading first resource

I am writing a Spring Batch application using Spring Boot 1.5, following are my classes : -
CustomMultiResourceItemReader.java
#StepScoped
#Component
public class CustomMultiResourceItemReader
extends MultiResourceItemReader<MyDTO> {
public MultiResourceXmlItemReader(
#NonNull final MyResourceAwareItemReader itemReader,
#NonNull final ApplicationContext ctx)
throws IOException {
setResources(
ctx.getResources(
String.format(
"file:%s/*.xml", "~/data"))); // gives me a Resource[] array fine
setDelegate(itemReader);
}
#PreDestroy
void destroy() {
close();
}
}
MyResourceAwareItemReader.java
#RequiredArgsConstructor
#StepScope
#Component
#Slf4j
public class MyResourceAwareItemReader
implements ResourceAwareItemReaderItemStream<MyDTO> {
private static final String RESOURCE_NAME_KEY = "RESOURCE_NAME_KEY";
#NonNull private final Unmarshaller unmarshaller; // JaxB Unmarshaller
private Resource resource;
#Override
public void setResource(Resource resource) {
this.resource = resource; // **gets called only once**
}
#Override
public MyDTO read() throws Exception {
final MyDTO dto = (MyDTO) unmarshaller.unmarshal(resource.getFile()); // Standard JaxB unmarshalling.
return dto;
}
#Override
public void open(ExecutionContext executionContext) throws ItemStreamException {
if (executionContext.containsKey(RESOURCE_NAME_KEY)) {
} else if (resource != null) {
executionContext.put(RESOURCE_NAME_KEY, resource.getFilename());
}
}
#Override
public void update(ExecutionContext executionContext) throws ItemStreamException {
if (resource != null) executionContext.put(RESOURCE_NAME_KEY, resource.getFilename());
}
#Override
public void close() throws ItemStreamException {}
}
The problem is the setResource method in the delegate reader (MyResourceAwareItemReader.java) gets called only once at the beginning; while the read method gets called multiple times, as a result I read the same item multiple times, instead of reading the next item as expected.
I have also browsed the source code of MultiResouceItemReader in Spring Batch, it seems like, the read method of the delegate class is supposed to return null after each item is read, I can clearly see my code doesnt seem to do that.
I am bit lost how to make this work. Any help is much appreciated
Looking further into it, ItemReader documentation, clearly details that reader must return null at the end of the input data set. So basically I implemented my ItemReader with a boolean flag as follows: -
#RequiredArgsConstructor
#StepScope
#Component
#Slf4j
public class MyResourceAwareItemReader
implements ResourceAwareItemReaderItemStream<MyDTO> {
private static final String RESOURCE_NAME_KEY = "RESOURCE_NAME_KEY";
#NonNull private final Unmarshaller unmarshaller; // JaxB Unmarshaller
private Resource resource;
private boolean isResourceRead;
#Override
public void setResource(Resource resource) {
this.resource = resource;
isResourceRead = false;
}
#Override
public MyDTO read() throws Exception {
if(isResourceRead == true) return null;
final MyDTO dto = (MyDTO) unmarshaller.unmarshal(resource.getFile());
isResourceRead = true;
return dto;
}
#Override
public void open(ExecutionContext executionContext) throws ItemStreamException {
if (executionContext.containsKey(RESOURCE_NAME_KEY)) {
} else if (resource != null) {
executionContext.put(RESOURCE_NAME_KEY, resource.getFilename());
}
}
#Override
public void update(ExecutionContext executionContext) throws ItemStreamException {
if (resource != null) executionContext.put(RESOURCE_NAME_KEY, resource.getFilename());
}
#Override
public void close() throws ItemStreamException {}
}
MultiResourceItemReader does not returns null each time. If there are no more resources to read the it returns NULL otherwise it returns the next resources to the delegate that means – Your actual reader
I can see problem in your read() method . you are not moving to next file. As you are implementing you own MultiResourceItemReader It’s your responsibility to move to next resources item.
This is how it is implanted in MultiResourceItemReader . You will need your own similar implementation.
private T readNextItem() throws Exception {
T item = delegate.read();
while (item == null) {
currentResource++;
if (currentResource >= resources.length) {
return null;
}
delegate.close();
delegate.setResource(resources[currentResource]);
delegate.open(new ExecutionContext());
item = delegate.read();
}
return item;
}
You need to maintain index of resources array . Please check implementation of MultiResourceItemReader. You need to do exactly similar way

Resources