"LinkageError-> загрузчик 'app' попытался определить дубликат класса" после маркировки классов как @Transactional

После маркировки класса как @Transactional (см. ниже), я получаю LinkageErrors о "попытке определения дублирующегося класса", когда мое приложение перезагружается, когда пользователь еще вошел в систему.

Ошибка:

org.springframework.security.authentication.InternalAuthenticationServiceException: java.lang.LinkageError-->loader 'app' (instance of jdk.internal.loader.ClassLoaders$AppClassLoader) attempted duplicate class definition for [redacted].AdminUserDao$$FastClassBySpringCGLIB$$2ffa020e.
        at org.springframework.security.authentication.dao.DaoAuthenticationProvider.retrieveUser(DaoAuthenticationProvider.java:126)

Где adminUserDao - это дао, которое мы используем в нашем пользовательском UserDetailsService для загрузки пользователей во время аутентификации.

Оригинальный AdminUserDao.java

import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class AdminUserDao {
    private final SessionFactory sessionFactory;

    @Autowired
    public AdminUserDao(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public AdminUser getUserByEmailAddress(String emailAddress) {
        String hql = "from " + AdminUser.class.getName()
                + " admin_user where admin_user.emailAddress = :emailAddress";
        Session session = null;
        try {
          session = sessionFactory.openSession();
          return session
                .createQuery(hql, AdminUser.class)
                .setParameter("emailAddress", emailAddress)
                .uniqueResult();
       } finally {
          if (session != null && session.isOpen()) {
            session.close();
          }
    }
}

Транзакционный AdminUserDao.java

import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@Repository
public class AdminUserDao {
    private final SessionFactory sessionFactory;

    @Autowired
    public AdminUserDao(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public AdminUser getUserByEmailAddress(String emailAddress) {
        String hql = "from " + AdminUser.class.getName()
                + " admin_user where admin_user.emailAddress = :emailAddress";
        return sessionFactory.getCurrentSession()
                .createQuery(hql, AdminUser.class)
                .setParameter("emailAddress", emailAddress)
                .uniqueResult();
    }
}

Менеджер транзакций Бин

@Bean
public HibernateTransactionManager transactionManager(@Qualifier("sessionFactory") SessionFactory sessionFactory) {
    return new HibernateTransactionManager(sessionFactory);
}

Действия по воспроизведению:

  1. Авторизоваться
  2. Перезапустите приложение (без выхода)
  3. Перезагрузите страницу (пользователь должен войти в систему)
  4. Выйти
  5. Попытайтесь войти снова с тем же пользователем. Ошибка срабатывает, когда Spring пытается аутентифицировать пользователя.

Я видел, как другие в переполнении стека сталкивались с этой проблемой, когда они неправильно определяли пользовательский ClassLoader ( здесь); Однако я не использую пользовательский ClassLoader.

Потенциально релевантные версии зависимостей:

  • Ява: 11
  • Spring Boot: 2.1.1. ВЫПУСК
  • Весна: 5.1.3. ВЫПУСК
  • Spring Security: 5.1.4. ВЫПУСК

0 ответов

Приведенная выше ссылка: https://docs.spring.io/spring/docs/3.0.0.M3/reference/html/ch08s06.html по jwilner дает правильный намек (Спасибо;))

Аннотация @Transactional означает, что Spring создает для вас прокси-класс, который обертывает ваш метод чем-то вроде:

GeneratedProxy {

void proxiedMethod(){
   try{ 
     tx.begin();

     yourClass.yourMethod(); 

     tx.commit();
   }
   .. plus usual rollback and error stuff .. 
}
}

Теперь у Spring есть два варианта (см. https://botproxy.net/docs/how-to/mismatched-proxy-types-jdk-vs-cglib-when-using-enablecaching-with-custom-aop-advice/):

если ваш код реализует интерфейс, он будет использовать прокси JDK и предоставит все методы интерфейса:

(см., например, https://www.byteslounge.com/tutorials/jdk-dynamic-proxies для примера прокси JDK)

если ваш код не реализует какой-либо интерфейс, он создаст прокси CGLIB (и по существу угадывает, какие методы раскрыть)

Наконец, если он сгенерирует прокси CGLIB, он создаст класс среды выполнения, например AuthcUserService$$EnhancerBySpringCGLIB$$5c9dcb68

прокси JDK он создаст вместо класса com.sun.proxy.$Proxy1631

Эти два будут отнесены к правильному классу в методе установщика (сгенерированного установщика):

в первом случае вы можете использовать сам класс в родительском классе, во втором случае вы должны использовать интерфейс, иначе вы получите org.springframework.beans.factory.BeanNotOfRequiredTypeException

В вашем случае вы можете избавиться от @Transactional, в моем случае я не могу избавиться от @Transactional, потому что я выполняю реальную транзакцию в wildfly через несколько БД.

В заключение, это происходит в java 13, вероятно, из-за изменений в jee и отражении, и на самом деле в jdk1.8 приведенный ниже код совершенно не знал об этой механике.

Другие вопросы по тегам