Laying down the law with the Maven Enforcer Plugin

We recently moved an application to a new server, with an updated installation of Java 8. It didn’t take long to discover that Amazon S3 accesses were failing with an authentication error, and it didn’t take much longer to discover that this problem was already fixed in the version of the AWS Java SDK we were using.

com.amazonaws.services.s3.model.AmazonS3Exception: AWS authentication requires a valid Date or x-amz-date header

The real problem was that we had many dependencies on different versions of Joda Time, and AWS had lost the battle - we were actually running Joda Time 2.0.

It was time to stop this happening again, with the Maven Enforcer Plugin. It’s possible to fail the build if we ever reference two versions of the same dependency:

Spring 4 and Quartz 2 Integration with Custom Annotations

I’m integrating Quartz scheduling into an application, and was looking for an annotation based approach to configuration. I quickly found the SivaLabs implementation, which works for Quartz 1.8. I’ve made a few changes to use this with Spring 4.1.4 and Quartz 2.2.3.

The custom annotation remains the same:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
@Scope("prototype")
public @interface QuartzJob {
    String name();
    String group() default "DEFAULT_GROUP";
    String cronExp();
}

The major change is that I have replaced the ApplicationListener class with a bean that performs a single scan of a selected package. I chose this approach because my configuration does not change once loaded, and I wanted to throw a fatal exception on any error in Quartz configuration. I’ve refactored to add each job to the scheduler as it is discovered, rather than building an intermediate list.

public class QuartzJobScanner
{
    @Autowired
    private Scheduler scheduler;

    private static final Logger log = LoggerFactory.getLogger(QuartzJobScanner.class);

    private final String scanPackage;

    public QuartzJobScanner(String scanPackage) {
         this.scanPackage = scanPackage;
    }

    @PostConstruct
    public void scheduleJobs() throws Exception
    {
        ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
        // Filter to include only classes that have a particular annotation.

        provider.addIncludeFilter(new AnnotationTypeFilter(QuartzJob.class));
        // Find classes in the given package (or subpackages)

        Set<BeanDefinition> beans = provider.findCandidateComponents(scanPackage);

        for (BeanDefinition bd: beans)
            scheduleJob(bd);
    }

The CronTriggerBean and JobDetailBean from Spring 3 are gone in Spring 4. The new code builds the job using the Quartz 2 fluent builders, rather than Spring’s factory beans:

    private void scheduleJob(BeanDefinition bd) throws Exception
    {
        Class<?> beanClass = Class.forName(bd.getBeanClassName());
        QuartzJob quartzJob = beanClass.getAnnotation(QuartzJob.class);

        // Sanity check

        if(Job.class.isAssignableFrom(beanClass) && quartzJob != null)
        {
            @SuppressWarnings("unchecked") Class<? extends Job> jobClass = (Class<? extends Job>)(beanClass);

            log.info("Scheduling quartz job: " + quartzJob.name());
            JobDetail job = JobBuilder.newJob(jobClass)
                    .withIdentity(quartzJob.name(), quartzJob.group())
                    .build();
            Trigger trigger = TriggerBuilder.newTrigger()
                    .withSchedule(CronScheduleBuilder.cronSchedule(quartzJob.cronExp()))
                    .withIdentity(quartzJob.name() + "_trigger", quartzJob.group())
                    .forJob(job)
                    .build();

            scheduler.scheduleJob(job, trigger);
        }
    }
}

The job factory is unchanged from the original article, and the XML configuration looks like this:

<context:component-scan base-package="uk.co.humboldt.Application.Services" />
<bean class="uk.co.humboldt.Application.Services.QuartzJobScanner">
        <constructor-arg value="uk.co.humboldt.Application.Services"/>
</bean>
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="jobFactory">
            <bean class="uk.co.humboldt.Application.Services.QuartzJobFactory"/>
        </property>
</bean>

At this point jobs are automatically configured as long they:

  • implement the Job interface
  • have the QuartzJob annotation
  • are in a package scanned by Spring, and by the QuartzJobScanner

Full code here.

A Breaking Change

We have a monolith. It’s not the biggest monolith, but it does contain a lot of Java libraries, managed with Maven. This week an upgrade to one package caused a distant side effect.

Our monolith is a web application, and uses an embedded Jetty server. It also uses Jasper reports, compiling jrxml files on demand. This week, our builds stopped compiling reports, with an exception that Jasper could not find javac. The first thought was that somebody had changed the server install from a JDK to a JRE, leaving the compiler unavailable. But after installing the latest JDK, and checking the system path, the error changed:

net.sf.jasperreports.engine.JRException: Errors were encountered when compiling report expressions class file:
C:\Users\adrian\Workspaces\Customer\App\App-Web\j491845presale_seller_1464295746680_612367.java:227: error: cannot find symbol
                value = DATEFORMAT(((java.sql.Timestamp)field_sale_start.getValue()), "dd/MM/YYYY"); //$JR_EXPR_ID=2$
                        ^
  symbol:   method DATEFORMAT(Timestamp,String)
  location: class j491845presale_seller_1464295746680_612367

As jasperreports-functions-5.6.1.jar was in the classpath, there was clearly another problem. Why was Jasper searching for javac at all, when Eclipse JDT was available?

 <dependency>
            <groupId>org.eclipse.jdt.core.compiler</groupId>
            <artifactId>ecj</artifactId>
            <version>4.4</version>
</dependency>

The next step was to go through our commits to find the point where things went wrong. It didn’t take long to discover the breakage was caused by an upgrade from Jetty 9.0 (2013) to Jetty 9.3 (2016). But how had this broken Jasper?

Stepping through Jasper initialisation reveals that Jasper attempts to load org.eclipse.jdt.internal.compiler.env.AccessRestriction in order to discover which version of JDT is present. This attempt fails, even though the class is present on the classpath.

This turned my debugging attention to the classloader. Jetty includes a WebAppClassloader, and setting a breakpoint in its loadClass method revealed that the JDT class was identified as a server class. Knowing this cause, it didn’t take long to find the commit that broke Jasper on Github.

As our application uses Jetty as an embedded web server, I disabled the WebAppClassloader like this, and Jasper reports worked again:

WebAppContext wac = new WebAppContext();
// As we use Jasper Reports dynamic compiling, ensure Jetty uses the standard system classloader

// so that Jasper Reports can load ECJ

wac.setClassLoader(ClassLoader.getSystemClassLoader());