Stopping Tomcat Doesn't Delete Derby db.lck

一个人想着一个人 提交于 2019-12-10 09:55:37

问题


(Edit: I've added a bounty to the question. I found a workaround (posted as an answer below), but I'm hoping somebody can explain why the workaround was necessary in the first place.)

I have a Spring webapp that connects to a Derby database during development. This works fine the first time I run the webapp, but in subsequent runs it fails during startup with the "Another instance of Derby may have already booted the database" SQLException.

I understand that this is because the connection to Derby isn't being closed when I shutdown Tomcat, even though I would expect Spring to handle that automatically. So my question is, how do I disconnect from Derby correctly? Not only during manually stopping Tomcat, but also during hot deploying of a new .war file?

I'd like to avoid using a Derby server, and I'm also using annotations instead of XML configuration. Here was my original PersistConfig class:

package com.example.spring.config;

import java.beans.PropertyVetoException;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.derby.jdbc.EmbeddedDataSource;
import org.hibernate.SessionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.jdbc.datasource.embedded.ConnectionProperties;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseConfigurer;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.hibernate4.HibernateExceptionTranslator;
import org.springframework.orm.hibernate4.HibernateTransactionManager;
import org.springframework.orm.hibernate4.LocalSessionFactoryBean;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@ComponentScan({"com.example.spring.dao.jpa"})
@EnableTransactionManagement // <-- enable @Transactional annotations for spring @Component and stereotypes
public class PersistConfig{


    @Bean
    public HibernateExceptionTranslator exceptionTranslator() {
        return new HibernateExceptionTranslator();
    }

    @Bean
    public LocalSessionFactoryBean localSessionFactoryBean(DataSource dataSource, JpaVendorAdapter vendorAdapter) {
        LocalSessionFactoryBean localSessionFactoryBean = new LocalSessionFactoryBean();
        localSessionFactoryBean.setDataSource(dataSource);
        localSessionFactoryBean.setPackagesToScan("com.example.one", "com.example.two");

        Properties properties = new Properties();
        properties.putAll(vendorAdapter.getJpaPropertyMap());
        localSessionFactoryBean.setHibernateProperties(properties);

        return localSessionFactoryBean;
    }

    @Bean
    public HibernateTransactionManager hibernateTransactionManager(SessionFactory sessionFactory) {
        HibernateTransactionManager hibernateTransactionManager = new HibernateTransactionManager(sessionFactory);

        return hibernateTransactionManager;
    }

    @Configuration
    public static class DevelopmentConfig{
        @Bean
        public DataSource dataSource() throws SQLException, PropertyVetoException {



            DataSource dataSource = new SimpleDriverDataSource(new org.apache.derby.jdbc.EmbeddedDriver(), "jdbc:derby:C:\\Users\\Kevin\\Desktop\\DerbyDB", "", "");

            System.out.println("RETURNING DATASOURCE");

            return dataSource;

        }

        @Bean
        JpaVendorAdapter vendorAdapter() {
            HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();

            vendorAdapter.setDatabase(Database.DERBY);
            vendorAdapter.setDatabasePlatform("org.hibernate.dialect.DerbyDialect");
            vendorAdapter.setGenerateDdl(true);
            vendorAdapter.setShowSql(true);

            vendorAdapter.getJpaPropertyMap().put("hibernate.hbm2ddl.auto", "update");
            vendorAdapter.getJpaPropertyMap().put("hbm2ddl.auto", "update");

            return vendorAdapter;
        }
    }
}

I tried adding a shutdown hook to the whole JVM using Runtime.addShutdownHook() where I manually disconnect from the Derby database, but that is seemingly never fired.

I was then told to look into the EmbeddedDatabaseConfigurer interface to add a Spring shutdown callback where I manually close the database connection, and this is what I came up with:

package com.example.spring.config;

import java.beans.PropertyVetoException;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.derby.jdbc.EmbeddedDataSource;
import org.hibernate.SessionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.jdbc.datasource.embedded.ConnectionProperties;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseConfigurer;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.hibernate4.HibernateExceptionTranslator;
import org.springframework.orm.hibernate4.HibernateTransactionManager;
import org.springframework.orm.hibernate4.LocalSessionFactoryBean;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@ComponentScan({"com.example.spring.dao.jpa"})
@EnableTransactionManagement // <-- enable @Transactional annotations for spring @Component and stereotypes
public class PersistConfig implements EmbeddedDatabaseConfigurer {


    @Bean
    public HibernateExceptionTranslator exceptionTranslator() {
        return new HibernateExceptionTranslator();
    }

    @Bean
    public LocalSessionFactoryBean localSessionFactoryBean(DataSource dataSource, JpaVendorAdapter vendorAdapter) {
        LocalSessionFactoryBean localSessionFactoryBean = new LocalSessionFactoryBean();
        localSessionFactoryBean.setDataSource(dataSource);
        localSessionFactoryBean.setPackagesToScan("com.example.one", "com.example.two");

        Properties properties = new Properties();
        properties.putAll(vendorAdapter.getJpaPropertyMap());
        localSessionFactoryBean.setHibernateProperties(properties);

        return localSessionFactoryBean;
    }

    @Bean
    public HibernateTransactionManager hibernateTransactionManager(SessionFactory sessionFactory) {
        HibernateTransactionManager hibernateTransactionManager = new HibernateTransactionManager(sessionFactory);

        return hibernateTransactionManager;
    }

    @Configuration
    public static class DevelopmentConfig{
        @Bean
        public DataSource dataSource() throws SQLException, PropertyVetoException {



            DataSource dataSource = new SimpleDriverDataSource(new org.apache.derby.jdbc.EmbeddedDriver(), "jdbc:derby:C:\\Users\\Kevin\\Desktop\\DerbyDB", "", "");

            System.out.println("RETURNING DATASOURCE");

            return dataSource;

        }

        @Bean
        JpaVendorAdapter vendorAdapter() {
            HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();

            vendorAdapter.setDatabase(Database.DERBY);
            vendorAdapter.setDatabasePlatform("org.hibernate.dialect.DerbyDialect");
            vendorAdapter.setGenerateDdl(true);
            vendorAdapter.setShowSql(true);

            vendorAdapter.getJpaPropertyMap().put("hibernate.hbm2ddl.auto", "update");
            vendorAdapter.getJpaPropertyMap().put("hbm2ddl.auto", "update");

            return vendorAdapter;
        }       
    }

    @Override
    public void configureConnectionProperties(ConnectionProperties properties, String databaseName) {

        System.out.println("CONFIGURE");

        properties.setDriverClass(org.apache.derby.jdbc.EmbeddedDriver.class);
        properties.setUrl("jdbc:derby:C:\\Users\\Kevin\\Desktop\\DerbyDB");
    }

    @Override
    public void shutdown(DataSource ds, String databaseName) {

        System.out.println("SHUTTING DOWN");

        try {
            DriverManager.getConnection("jdbc:derby:C:\\Users\\Kevin\\Desktop\\DerbyDB;shutdown=true");
        } 
        catch (SQLException e) {
            e.printStackTrace();
        }
    }

}

However, neither the configureConnectionProperties() function nor the shutdown() function seem to be called. I obviously don't know what I'm doing, so any pointers are greatly appreciated.


回答1:


EDIT : add a precision of restarting embedded Derby database and a probably simpler solution.

I could reproduce at least partially the problem, understand it and fix it. But I cannot say why BoneCP works fine. I simply noticed that if I waited enough between shutting down tomcat and restarting it again, it worked. I suppose that BoneCP do not access immediately the database, and waits enough until first real connection.

First the problem : when using Derby as an embedded database, the database is booted at first connection, but it has to be explicitely shutted down. If it is not, the db.lock file is not deleted and further application may experience problems booting the database again. Nothing exists either in tomcat or (by default) in spring to automatically shutdown such a database.

Next, why your attempt using an EmbeddedDatabaseConfigurer didn't work : EmbeddedDatabaseConfigurer is not a magic marker and inheriting it in a class is not enough to have spring automatically use it. It is simply an interface that must be implemented by a configurer to allow an EmbeddedDatabaseFactory to use it.

Finally the fix. You should not use SimpleDriverDataSource to get your connections from an embedded Derby database but an EmbeddedDatabaseFactory. Spring by default knows Derby embedded database, and you can configure the factory by simply setting the type ... but that only works for in memory databases and you have a file database ! Would have been too simple ... You must inject the factory with a configurer to have all being ok.

And now the code (starting from your first version) :

@Configuration
public static class DevelopmentConfig{
    EmbeddedDatabaseFactory dsFactory;

    public DevelopmentConfig() {
        EmbeddedDatabaseConfigurer configurer = new EmbeddedDatabaseConfigurer() {
            @Override
            public void configureConnectionProperties(ConnectionProperties properties, String databaseName) {
                System.out.println("CONFIGURE");

                properties.setDriverClass(org.apache.derby.jdbc.EmbeddedDriver.class);
                properties.setUrl("jdbc:derby:C:\\Users\\Kevin\\Desktop\\DerbyDB");
            }

            @Override
            public void shutdown(DataSource dataSource, String databaseName) {
                final String SHUTDOWN_CODE = "XJ015";
                System.out.println("SHUTTING DOWN");

                try {
                    DriverManager.getConnection("jdbc:derby:;shutdown=true");
                } catch (SQLException e) {
                    // Derby 10.9.1.0 shutdown raises a SQLException with code "XJ015"
                    if (!SHUTDOWN_CODE.equals(e.getSQLState())) {
                        e.printStackTrace();;
                    }
                }
            }
        };
        dsFactory = new EmbeddedDatabaseFactory();
        dsFactory.setDatabaseConfigurer(configurer);
    }

    @Bean
    public DataSource dataSource() throws SQLException, PropertyVetoException {

        System.out.println("RETURNING DATASOURCE");

        return dsFactory.getDatabase();
    }

    // remaining of code unchanged

This way, I can hot reload the war, and when tomcat is closed, the db.lock is normally destroyed.

Edit: In case of problems, Derby documentation advices to add the following command to restart a database after a shutdown : Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance(); . It could be the last instruction of the configureConnectionProperties method.

But in fact, the solution could be even simpler. What really needs to be added to your config is a proper shutdown of the embedded driver (and eventually a restart). So a simple PreDestroy (and eventualy `@PostConstruct) annotated method should be enough :

@Configuration
public static class DevelopmentConfig{

    @PreDestroy
    public void shutdown() {
        final String SHUTDOWN_CODE = "XJ015";
        System.out.println("SHUTTING DOWN");

        try {
            DriverManager.getConnection("jdbc:derby:;shutdown=true");
        } catch (SQLException e) {
            // Derby 10.9.1.0 shutdown raises a SQLException with code "XJ015"
            if (!SHUTDOWN_CODE.equals(e.getSQLState())) {
                e.printStackTrace();
            }
        }
    }

    /* if needed ...
    @PostConstruct
    public void init() throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance();
    }
    */
    @Bean
    public DataSource dataSource() throws SQLException, PropertyVetoException {

        DataSource dataSource = new SimpleDriverDataSource(new org.apache.derby.jdbc.EmbeddedDriver(), "jdbc:derby:C:\\Users\\Kevin\\Desktop\\DerbyDB", "", "");

        System.out.println("RETURNING DATASOURCE");

        return dataSource;

    }

    // remaining of code unchanged

The main interest of this variant is that you can choose your datasource, from a SimpleDriverDataSource to a real pool.




回答2:


I figured out a fix to the problem, although I don't really understand why this fix works. It turns out that using BoneCP to configure the DataSource fixes the problem, or at least covers it up.

public DataSource dataSource() throws SQLException, PropertyVetoException {
            BoneCPConfig config = new BoneCPConfig();
            config.setUsername("");
            config.setPassword("");
            config.setJdbcUrl("jdbc:derby:C:\\Users\\Kevin\\Desktop\\DerbyDB");

            BoneCPDataSource dataSource = new BoneCPDataSource(config);
            dataSource.setDriverClass("org.apache.derby.jdbc.EmbeddedDriver");

            return dataSource;
}

Stranger still, the db.lck file is still never deleted, but I don't see any errors at all, and it seems to be working fine.

I'm adding this as an answer in case somebody else has a similar problem, but I'm leaving the question open in case somebody can explain to me why this fixes the problem.




回答3:


You can use a LifeCycleListener which executes shutdown procedure of Derby if you defined Embedded Derby as a DataSource (Resource) of Tomcat.

Here's a sample implementation of LifeCycleListener for Tomcat8, and detail of setup.



来源:https://stackoverflow.com/questions/24491328/stopping-tomcat-doesnt-delete-derby-db-lck

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!