My company has a OAuth 2 secured API that I need to access from a Spring Batch or Scheduled process. I can access this API from our web applications using the following code:
First I created a WebClient bean
@Configuration
public class ScimOAuthConfiguration
{
/**
* This class creates and returns a WebClient
* that has been built to authenticate with
* SCIM using OAuth2
*
* @param clientRegistrationRepository
* @param authorizedClientRepository
* @return WebClient
*/
@Bean
WebClient scimWebClient(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository)
{
//Creates a Servlet OAuth2 Authorized Client Exchange Filter
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrationRepository, authorizedClientRepository);
//Returns WebClient with OAuth2 filter applied to it
return WebClient.builder().apply(oauth.oauth2Configuration()).build();
}
}
The userSearch() method below just makes a request to an end point, and converts the results into some classes that I have created.
@Component
@Slf4j
public class UserSearchService {
private final UserUtil userUtil;
private final WebClient webClient;
public UserSearchService(UserUtil userUtil, WebClient webClient) {
this.userUtil = userUtil;
this.webClient = webClient;
}
/**
* This method will query the SCIM API and return the results.
*/
public Schema<User> userSearch(UserSearchForm form) {
if (form.getCwsId().isEmpty() && form.getLastName().isEmpty() && form.getFirstName().isEmpty() && form.getEmail().isEmpty()) {
return null;
}
//generate the request URI
String uri = userUtil.generateSearchURL(form);
//Creates a User Schema object to contain the results of the web request
Schema<User> users = null;
try {
users = this.webClient.get()
.uri(uri)
.attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId("scim"))
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Schema<User>>() {
})
.block();
} catch (WebClientResponseException ex) {
log.error(ex.getMessage(), ex);
}
return users;
}
}
When I call this function in a controller or rest endpoint everything works just fine. Example:
@RestController
@Slf4j
public class UserSearchController {
@Autowired
UserSearchService service;
@PostMapping(value="/userSearch")
public Schema<User> userSearch(@RequestBody UserSearchForm form) {
return service.userSearch(form);
}
}
No problems so far...
I have been given a task that requires me to create a Spring Batch job that will run at midnight every night. Part of this batch job will require me to gather user information via the secured API. I would prefer to put this batch job inside of my web application project. The goal is to have this web application running on a server, and every night at midnight start this batch job that simply sends out some automated reminder emails. It would be nice to have everything be self-contained and in one repository. Here is my simplified example of the batch job that tries to access the secured API:
@Configuration
@Slf4j
public class ReminderEmailJob {
@Autowired
JobBuilderFactory jobBuilderFactory;
@Autowired
StepBuilderFactory stepBuilderFactory;
@Autowired
UserSearchService userSearchService;
@Bean
public Job job() {
return this.jobBuilderFactory.get("basicJob")
.start(step1())
.build();
}
@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1").tasklet((contribution, chunkContext) -> {
UserSearchForm form = new UserSearchForm("", "", "userNameHere", "");
Schema<User> user = userSearchService.userSearch(form);
System.out.println(user);
log.info("Hello, world!");
return RepeatStatus.FINISHED;
}).build();
}
}
Here is a @Scheduled method that will run this batch job every night at midnight:
@Service
@Configuration
@EnableScheduling
@Slf4j
public class EmailScheduler {
@Autowired
ReminderEmailJob job;
@Autowired
JobLauncher jobLauncher;
@Scheduled(cron = "0 0 0 * * *")
public void runAutomatedEmailBatchJob() throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
jobLauncher.run(job.job(), new JobParametersBuilder().addDate("launchDate", new Date()).toJobParameters());
}
}
When I try running the batch job I get the following error:
2020-02-19 10:21:38,347 ERROR [scheduling-1] org.springframework.batch.core.step.AbstractStep: Encountered an error executing step step1 in job basicJob
java.lang.IllegalArgumentException: request cannot be null
at org.springframework.util.Assert.notNull(Assert.java:198)
at org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository.loadAuthorizedClient(HttpSessionOAuth2AuthorizedClientRepository.java:47)
at org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository.loadAuthorizedClient(AuthenticatedPrincipalOAuth2AuthorizedClientRepository.java:75)
at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.populateDefaultOAuth2AuthorizedClient(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:321)
at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.lambda$null$1(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:187)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.attributes(DefaultWebClient.java:234)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.attributes(DefaultWebClient.java:153)
at org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.lambda$defaultRequest$2(ServletOAuth2AuthorizedClientExchangeFilterFunction.java:184)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.initRequestBuilder(DefaultWebClient.java:324)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.exchange(DefaultWebClient.java:318)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.retrieve(DefaultWebClient.java:368)
at com.cat.reman.springbootvuejs.dao.service.cgds.UserSearchService.userSearch(UserSearchService.java:53)
at com.cat.reman.springbootvuejs.batch.ReminderEmailJob.lambda$step1$0(ReminderEmailJob.java:47)
at org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:407)
at org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:331)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140)
at org.springframework.batch.core.step.tasklet.TaskletStep$2.doInChunkContext(TaskletStep.java:273)
at org.springframework.batch.core.scope.context.StepContextRepeatCallback.doInIteration(StepContextRepeatCallback.java:82)
at org.springframework.batch.repeat.support.RepeatTemplate.getNextResult(RepeatTemplate.java:375)
at org.springframework.batch.repeat.support.RepeatTemplate.executeInternal(RepeatTemplate.java:215)
at org.springframework.batch.repeat.support.RepeatTemplate.iterate(RepeatTemplate.java:145)
at org.springframework.batch.core.step.tasklet.TaskletStep.doExecute(TaskletStep.java:258)
at org.springframework.batch.core.step.AbstractStep.execute(AbstractStep.java:203)
at org.springframework.batch.core.job.SimpleStepHandler.handleStep(SimpleStepHandler.java:148)
at org.springframework.batch.core.job.AbstractJob.handleStep(AbstractJob.java:399)
at org.springframework.batch.core.job.SimpleJob.doExecute(SimpleJob.java:135)
at org.springframework.batch.core.job.AbstractJob.execute(AbstractJob.java:313)
at org.springframework.batch.core.launch.support.SimpleJobLauncher$1.run(SimpleJobLauncher.java:144)
at org.springframework.core.task.SyncTaskExecutor.execute(SyncTaskExecutor.java:50)
at org.springframework.batch.core.launch.support.SimpleJobLauncher.run(SimpleJobLauncher.java:137)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.batch.core.configuration.annotation.SimpleBatchConfiguration$PassthruAdvice.invoke(SimpleBatchConfiguration.java:127)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
at com.sun.proxy.$Proxy131.run(Unknown Source)
at com.cat.reman.springbootvuejs.dao.service.emailService.EmailScheduler.jakeTest(EmailScheduler.java:162)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84)
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:514)
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java:305)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:300)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.base/java.lang.Thread.run(Thread.java:844)
Which leads me to believe that in the context of the web application we can access the WebClient bean that was created just fine. When it comes to the batch job it's a different story. I have found that these two instances will be run on different threads, and will not share the same information or context.
My question is how do I access a secured API in a similar fashion using Spring Batch? I have the OAuth token client id, client secret, etc. available. Thank you.
My question is how do I access a secured API in a similar fashion using Spring Batch? I have the OAuth token client id, client secret, etc. available
You can create a custom ItemReader
based on a OAuth2RestTemplate
to access the secured API. I found this example to be useful. Just make sure to read data in a paginated fashion to avoid loading all items in memory at once.
That said, I don't see the need to run this job in a web application. Running your EmailScheduler
as a standalone spring boot app is enough IMO.