Showing posts with label Upgrading. Show all posts
Showing posts with label Upgrading. Show all posts

Wednesday, August 23, 2023

Spring Batch - upgrade from 4.3.x to 5.0.x (breaking changes) . . .

It makes sense to expect some breaking changes between two major versions. I recently ran into this when upgrading a Spring Boot based Spring Batch application from Spring Boot 2.7.14 to Spring Boot 3.1.2.

The application is a Spring Boot command-line runnable app, which takes the batch job name(s) and input data file(s) (job related datafile arg, and filename) and processes those files by kicking off those jobs. This application required few major changes to get it functioning. I had to read the docs to identify few things that were already deprecated in 4.x and are removed in 5.x. also finding out on new things put in place.

Environment: Java 20, Spring Boot 2.7.14, Spring Boot 3.1.2, Spring Batch 4.3.8, Spring Batch 5.0.2, maven 3.9.3 on macOS Catalina 10.15.7

The following is the summary of high level changes. I am not going into details in this post as these changes are easy enough to understand at a high level.

1. The in memory map datasource in earlier versions was deprecated. Now it's removed. So, it requires to put in a configuration for this. The following is an example.
import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.support.JobRepositoryFactoryBean; import org.springframework.batch.support.transaction.ResourcelessTransactionManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; /** * In memory data source configuration for batch jobs. * Defines {@link DataSource}, {@link PlatformTransactionManager} and {@link JobRepository} beans. * * @author Giri * created Aug 16, 2023 */ @Configuration public class InMemoryBatchRepositoryConfig { @Bean public DataSource inMemoryDataSource() { EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); return builder.setType(EmbeddedDatabaseType.H2) .addScript("classpath:org/springframework/batch/core/schema-drop-h2.sql") .addScript("classpath:org/springframework/batch/core/schema-h2.sql") .generateUniqueName(true) .build(); } @Bean public PlatformTransactionManager resourceLessTransactionManager() { return new ResourcelessTransactionManager(); } @Bean public JobRepository jobRepository() throws Exception { JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean(); factory.setDataSource(inMemoryDataSource()); factory.setTransactionManager(resourceLessTransactionManager()); factory.afterPropertiesSet(); return factory.getObject(); } }

2. @EnableBatchProcessing annotation takes new properties: dataSourceRef, transactionManagerRef.
@Slf4j @Configuration @EnableBatchProcessing(dataSourceRef = "inMemoryDataSource", transactionManagerRef = "resourceLessTransactionManager") public class MyJobConfig { @Value("${data_filename:NONE}") String dataFilename; ... }

3. JobBuilderFactory, StepBuilderFactory are removed. Use JobBuilder and StepBuilder instead.
@Slf4j @Configuration @EnableBatchProcessing(dataSourceRef = "inMemoryDataSource", transactionManagerRef = "resourceLessTransactionManager") public class MyJobConfig { ... @Bean public Job myJob( JobRepository jobRepository, Step myJobStep, JobCompletionNotificationListener jobCompletionNotificationListener) { return new JobBuilder("myJob", jobRepository) .listener(jobCompletionNotificationListener) .flow(myJobStep) .end() .build(); } @Bean public Step myJobStep( JobRepository jobRepository, PlatformTransactionManager platformTransactionManager, ItemFailureLoggerListener itemFailureLoggerListener, StepListener stepListener){ var step = new StepBuilder("myJobStep", jobRepository) .<MyDomainObject, String> chunk(10, platformTransactionManager) .reader(myDomainObjectReader()) //input .processor(myDomainObjectProcessor(isGm5)) //transformer/processor .writer(myDomainObjectWriter())//output .listener((ItemProcessListener) itemFailureLoggerListener) .build(); step.registerStepExecutionListener(stepListener); return step; } ... }

4. Unlike previous versions, jobs won't start by setting the property: spring.batch.job.names to a comma separated names of jobs. Need to explicitly start the job launcher. The following is a code snippet of the main application which takes a job name passed from command line by property spring.batch.job.name and launches that job.

@Slf4j @SpringBootApplication public class BatchApplication implements CommandLineRunner, InitializingBean { @Value("${spring.batch.job.name:NONE}") private String jobName; @Autowired ApplicationContext applicationContext; @Autowired JobLauncher jobLauncher; @Override public void afterPropertiesSet() { try { validateJobName(); } catch (Exception ex) { log.error(ex.getMessage()); System.exit(SpringApplication.exit(applicationContext, () -> 1)); } } public static void main(String[] args) { SpringApplication app = new SpringApplicationBuilder(ScoringEtlApplication.class) .web(WebApplicationType.NONE) .logStartupInfo(false) .build(args); app.run(args); } @Override public void run(String... args) { log.debug("Beans Count:{} Beans:{}", applicationContext.getBeanDefinitionCount(), applicationContext.getBeanDefinitionNames()); Job job = (Job)applicationContext.getBean(jobName); JobParameters jobParameters = new JobParametersBuilder() .addString("jobID", String.valueOf(System.currentTimeMillis())) .toJobParameters(); try { jobLauncher.run(job, jobParameters); log.info("Finished running job(s): {} with args: {}", jobName, Arrays.stream(args).collect(Collectors.joining(", "))); } catch (JobExecutionAlreadyRunningException | JobRestartException | JobInstanceAlreadyCompleteException | JobParametersInvalidException e) { throw new RuntimeException(e); } } /** * Validates jobName and displays usage and appropriate error message upon validation failure. */ private void validateJobName() throws URISyntaxException { String errorMessage = null; if (jobName.isEmpty() || jobName.equals("NONE")) { errorMessage = "No job(s) specified. Please, specify valid job_name(s)"; } if(errorMessage != null) { String runningJar = this.getClass().getClassLoader().getClass() .getProtectionDomain() .getCodeSource() .getLocation() .toURI() .getPath(); // display Usage log.info(""" USAGE: Run a specific job: java -jar <runnableJar> \\ --spring.batch.job.name=<job_name> \\ --<job_arg_data_filename>="job_data_file.csv" """.replace("runningJar", runningJar)); throw new IllegalArgumentException(errorMessage); } else { List allJobNames = Arrays.stream(applicationContext.getBeanNamesForType(Job.class)).toList(); log.debug("Available Jobs: {}", allJobNames); log.info("Running Job: {}", jobName); } } }

Other classes referenced in Job configurations:
public class JobCompletionNotificationListener extends JobExecutionListenerSupport { @Value("${spring.batch.job.name:default-job}") private String jobName; @Override public void beforeJob(JobExecution jobExecution) { log.info("JOB: {} - About to start.", jobName); } @Override public void afterJob(JobExecution jobExecution) { if(jobExecution.getStatus() == BatchStatus.COMPLETED) { var from = jobExecution.getStartTime(); var to = jobExecution.getEndTime(); log.info("JOB: {} - Finished running. Took {} milliseconds. Verify results.", jobName, ChronoUnit.MILLIS.between(from, to)); } } } @Slf4j @Component public class ItemFailureLoggerListener extends ItemListenerSupport

TIPS

When running the command line application, if there are warnings: Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: MyJobConfig, then suppress it by setting the following  maven-compiler-plugin setting:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven-compiler-plugin.version}</version> <configuration> <source>${javac.source.version}</source> <target>${javac.target.version}</target> <release>${javac.release.version}</release> <compilerArgs> <arg>-parameters</arg> </compilerArgs> </configuration> </plugin>

References

Sunday, December 18, 2016

Upgrading Grails-2.2.1 to Grails-3: Static Assets take a BIG move . . .

I recently upgraded a Grails 2.2.1 web-app to Grails 3.2.1. It was a BIG move forward. Moving static assets (JavaScripts, CSS and Images) to their new assets directory, organizing & setting up directives/manifests in their new home directory, making all required changes to views & templates, testing all views for styles & images, and testing views with AJAX functionality involving JavaScripts... overall, it took a considerable amount my time during the whole upgrade efforts.

Following are some key points I have from my efforts:
  • Grails 3 doesn't come with Resources plugin and hence you will not have ResourceTagLib in your classpath. If you want, you can probably still use Resources plugin in Grails-3 app, but Asset-Pipeline plugin seems to be the viable option.
  • Static assets which can be handled by Asset-Pipeline plugin need a move from web-app directory to grails-app/assets directory, their new home. Files under web-app/css, web-app/js and web-app/images now go into grails-app/assets/stylesheetsgrails-app/assets/javascripts and grails-app/assets/images respectively.
  • Grails-3 comes with jquery-2.2.0.min.js and bootstrap.js(3.3.6). If your Grails 2.2.1 app was dependent on these, you probably had jquery-1.7.2.min.js and bootstrap.js(2.2.2). If so, you will be better off retaining older versions to start with the upgrade process to eliminate this new variance in upgrading-equation.
  • The recommended approach to upgrade Grails 2.x app to Grails 3.x is to first create a new Grails-3 application and start copying all artifacts from old to new locations. Grails-3 documentation's Upgrading section has very well documented details on old and new locations. With respect to static assets, when a new Grails-3 app is created, you will notice images, javascripts and stylesheets sub directories under grails-app/assets. Also, you will have a bunch of static assets already sitting in there. You may have to do some cleanup with these files.
  • Modularize your assets
    1. AppResources.groovy is Resources plugin’s way of modularizing JavaScripts and CSS files by grouping these static assets into modules. But Asset-Pipeline plugin minifies compresses all JavaScript & CSS files, also enables browser cache, and hence static assets are only served once for all pages. So, it may not be required to group/ modularize static assets. If truly needed, for every module (e.g. module1) in AppResources.groovy, an equivalent manifest/directive with module-name.js (e.g. modul1.js) file can be created listing all it’s dependencies.
    2. The directive files application.js and application.css are main manifest files for JavaScripts and CSS.
    3. If you have modularized static assets in Grails-2 app, your main static resource AppResources.groovy should be your reference for re-organizing your static assets in Grails-3 app to minimize changes in views and view templates.
    4. You can modularize your static assets in Grails-3 the same way as in Grails-2 app with no need for ApprRsources.groovy file but with equivalent module manifests/directives created.
    5. Create a one-to-one asset-pipeline directive/manifest file for each of your module defined in AppResources.groovy. For instance, if you have, let's say a common module defined listing all it's dependency resources (both JavaScripts and CSS files), create common.js and common.css asset-pipeline directives that list required JavaScript and CSS dependencies respectively in Grails-3 app.
    6. If you have many modules in your application, your grails-app/assets/javascripts and grails-app/assets/stylesheets will get cluttered and mixed with manifest files and actual assets. You will be better off keeping directives separate from actually assets by keeping actual javascript and css assets under grails-app/assets/javascripts/lib and grails-app/assets/stylesheets/shared sub-directories respectively so that asset-pipeline manifest files can be under main grails-app/assets/javascripts and grails-app/assets/stylesheets directories.
  • Modify views & view templates and change resources tags to equivalent asset-pipeline tags
    1. Replace all <r:script> </r:script> with <asset:script type=”text/javascript”> </asset:script>
    2. Replace <r:external file="/static/images/favicon.ico"/> with <asset:link rel='shortcut icon' href="favicon.ico" type="image/x-icon"/>
    3. Remove all <r:layoutResources/> in <head></head> and replace all <r:layoutResources/> at the very bottom of layout pages with <asset:deferredScripts/> (This is Asset-Pipeline plugin's equivalent of Resources plugin’s deferring scripts to the bottom of the page)
    4. If you have modularized assets in Grails 2.2.1 app, for example, for module 'module1'  replace all <r:require module=”module1”/> with <asset:stylesheet src=”module1”/> and <asset:javascript src=”module1”/> if module1 has both JavaScripts and CSSs in it.
  • If there are other static assets like pdf files that are referenced in views by grails resource tag or it's equivalent method call, it can safely be moved from Grails-2's web-app/pdf to Grails-3's grails-app/assets/pdf and be served by Assets-Pipeline plugin. These assets, like images, need no manifest/directive files and it simply works.

Summary

Grails moved away from Resources plugin in favor of Asset-Pipeline plugin starting from 2.4. Upgrading prior versions of Grails 2.4 apps to 3.x certainly requires considerable development and testing efforts with respect to static assets. So, just be prepared for this BIG move.

References

Resources Plugin Docs
Asset-Pipeline Plugin Docs
Asset-Pipeline Plugin - GitHub source code
Grails Team Blog Post on Migrate from Resources Plugin to Asset-Pipeline Plugin
Very nice Introduction to Asset Pipeline Plugin

My previous posts on Upgrading Grails application from 2.2.1 to 3.2.1

Sunday, November 20, 2016

Upgrading Grails-2 application to Grails-3: Spring Security Core Plugin differences . . .

I recently upgraded one of our Grails 2.2.1 with Spring Security core plugin 1.2.7.3 on Java 1.6 application to Grails 3.2.1 with Spring Security core plugin 3.1.1 on Java 1.8. By following the recommended path detailed out well enough in Grails 3 documentation, I got the following done before I got to the point of successfully running the application:
  • Upgraded one of our in-house plugins: ZipCityState
  • Reorganized Grails artifacts and other files as per Grails-3 app directory structure
  • Rewrote build and other configurations
  • Fixed several code compilation errors and issues resulted due to changed package names of several Grails frame-work classes and some classes that are deprecated and removed
  • Upgraded static resources like images, javascript and stylesheets from resources plugin to asset-pipeline plugin by re-organizing those files and creating appropriate asset-pipeline directives to mimic resource plugin's modules defined in AppResources.groovy
Once all the above are done, I had to make the following changes from the Security aspect for the application to successfully run, display and login:

Static Rules

Static rules are now List of Maps and not just a Map. I covered this in my previous post. Check it out.

Authentication

Change username and password form fields in login page (auth.gsp) from j_username and j_password to username and password.

If you have used UsernamePasswordAuthenticationFilter.SPRING_SECURITY_LAST_USERNAME_KEY somewhere in your code, you need to change that to SpringSecurityUtils.SPRING_SECURITY_LAST_USERNAME_KEY

If you have any pre authentication checks written by extending DefaultPreAuthenticationChecks, the hibernate session seems not created and attached to the current thread at this point.

If you run into any exception like the following, you may need to use either withTransaction or withSession method on the domain object to come over this.

org.springframework.dao.DataAccessResourceFailureException: Could not obtain current Hibernate Session; nested exception is org.hibernate.HibernateException: No Session found for current thread.

Password encryption algorithm differences

The application has an admin account created in the database only once with exists check from the Bootstrap. The login failed for admin user that was created by Grails 2.2.1 app and after upgrading to Grails 3.2.1 with the following exception:

ERROR org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/].[grailsDispatcherServlet] - Servlet.service() for servlet [grailsDispatcherServlet] in context with path [] threw exception [Filter execution threw an exception] with root cause java.lang.AssertionError: Salt value must be null when used with crypto module PasswordEncoder. Expression: salt. Values: salt = admin at org.codehaus.groovy.runtime.InvokerHelper.assertFailed(InvokerHelper.java:404) at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.assertFailed(ScriptBytecodeAdapter.java:650) at grails.plugin.springsecurity.authentication.encoding.BCryptPasswordEncoder.checkSalt(BCryptPasswordEncoder.groovy:49)

The error was bit puzzling and made me to comment out the following Spring security core plugin's configuration property set in application.groovy:

//grails.plugin.springsecurity.dao.reflectionSaltSourceProperty = 'username’

Commenting out that property revealed the issue with the following error:
WARN org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder - Encoded password does not look like BCrypt

After quickly reading through documents of both Grails-2 Spring Security Core Plugin and Grails-3 Spring Security Core Plugin, there was a special mention of Bcrypt algorithm in version 3 documentation. Also, it was specified up front in the Configuration Settings section of the doc that the plugin's default security settings are maintained in DefaultSecurityConfig.groovy file. I checked both plugin 2, plugin 3 and found the following differences:

Grails-3 plugin
password { algorithm = ‘bcrypt’ encodeHashAsBase64 = false bcrypt { logrounds = 10 } hash { iterations = 10000 } }

Grails-2 plugin
password.algorithm = 'SHA-256' password.encodeHashAsBase64 = false password.bcrypt.logrounds = 10

Differences are highlighted. The hash.iterations property is set to 10000 in Grails-3 plugin, but is not set explicitly in Grail-2 plugin. I had to add algorithm and hash.iterations explicitly to match Grails-2 plugin and retain the reflectionSaltSourceProperty in application.groovy.The following are the changes:

grails.plugin.springsecurity.password.algorithm = 'SHA-256' grails.plugin.springsecurity.password.hash.iterations = 1 grails.plugin.springsecurity.dao.reflectionSaltSourceProperty = 'username'

Summary

With static rule configuration changes, password encryption properties changes and code changes to auth.gsp and some security related classes, I was able to get the application successfully migrated from Grails 2.2.1 to Grails 3.2.1 along with upgraded Spring Security core plugin.

References

Grails 3.2.1 documentation
Grails Spring Security 2 documentation
Grails Spring Security 3 documentation


Sunday, November 13, 2016

Upgrading Grails-2 application to Grails-3 - Make static assets available . . .

Recently, I started upgrading a Grails web-application from Grails-2 to Grails-3, particularly from version 2.2.1 to 3.2.1. The perfect minor.patch(2.1) matching of both versions in this process was just a perfect timing-coincidence as Grails was at 2.2.1 when this app was developed and the latest Grails was at 3.2.1 when I started to upgrade.

The process of upgrading was not bad, but it required quite a bit of careful changes. I simply followed the very well documented Grails3 Documentation and get going this effort after successfully migrating an in-house plugin ZipCityState from Grails-2 to Grails-3. This deserves another blog post.

Static Assets

One of the web aspects that needed bit more effort in this upgrade was: Static Assets.

Grails-3 comes with asset-pipeline plugin. Static assets like images, javascript and stylesheets need an upgrade by moving from Resources plugin to Asset-pipeline plugin. There is a nice blog post on this from Grails Team and according to this, the viable option in Grails-3 is asset-pipeline and resources plugin is not available. I will write a bit more detailed post on this later.

After upgrading the application and taking care of several compilation and startup issues along the way, I was finally able to get the application started and running. The login page showed up but all the static assets like stylesheets, javascripts and images were totally missing. This made me look into all asset pipeline directives and related changes I made in migrating static assets to asset-pipeline. Everything looked good but the issue was just puzzling. After spending sometime finally I realized that static assets upgrade to asset-pipeline from Grails-2 to Grails-2 need some attention from security settings as well.

Grails Spring Security Plugin takes a pessimistic locks down approach and locks down all URLs that do not have an applicable URL mapping. 

Usually, Grails-2 security static rules look like:
grails.plugins.springsecurity.controllerAnnotations.staticRules = [ '/login/**': ['IS_AUTHENTICATED_ANONYMOUSLY'], '/js/**': ['IS_AUTHENTICATED_ANONYMOUSLY'], '/css/**': ['IS_AUTHENTICATED_ANONYMOUSLY'], '/images/**': ['IS_AUTHENTICATED_ANONYMOUSLY'], '/**': ['IS_AUTHENTICATED_FULLY'] ]

In Grails-3, add pattern 'assets/**' for static assets as they are now moved under grails-app/assets and get served by asset pipeline plugin through requests URLs like- http://my-app-url/assets/.  Make the static rules look like the following (highlighted is the change):
grails.plugin.springsecurity.controllerAnnotations.staticRules = [ [pattern: '/login/**', access: ['IS_AUTHENTICATED_ANONYMOUSLY']], [pattern: '/assets/**', access: ['IS_AUTHENTICATED_ANONYMOUSLY']], [pattern:'/**', access: ['IS_AUTHENTICATED_FULLY']] ]

There you go, asset pipeline plugin works and all static assets are available, get served and become visible!

References