sourcecode

스프링 의존성을 JPA EntityListener에 주입하는 방법

codebag 2023. 3. 14. 21:37
반응형

스프링 의존성을 JPA EntityListener에 주입하는 방법

스프링 의존성을 JPA EntityListener에 주입하려고 합니다.청취자 수업은 다음과 같습니다.

@Configurable(autowire = Autowire.BY_TYPE, dependencyCheck = true)
public class PliListener {

    @Autowired
    private EvenementPliRepository evenementPliRepository;

    @PostPersist
    void onPostPersist(Pli pli) {
        EvenementPli ev = new EvenementPli();
        ev.setPli(pli);
        ev.setDateCreation(new Date());
        ev.setType(TypeEvenement.creation);
        ev.setMessage("Création d'un pli");
        System.out.println("evenementPliRepository: " + evenementPliRepository);
        evenementPliRepository.save(ev);
    }


}

엔티티 클래스는 다음과 같습니다.

@RooJavaBean
@RooToString
@RooJpaActiveRecord
@EntityListeners(PliListener.class)
public class Pli implements Serializable{
...

다만, 나의 의존관계(즉,evenementPliRepository)는 항상 늘입니다.

누가 좀 도와줄래요?

상태 비저장 콩에 대한 의존성을 주입하는 해킹은 의존성을 "static"으로 정의하고 Spring이 의존성을 주입할 수 있도록 세터 메서드를 작성하는 것입니다(스프링의 의존성을 정적 의존성에 할당).

종속성을 정적으로 선언합니다.

static private EvenementPliRepository evenementPliRepository;

스프링이 주입할 수 있도록 메서드를 만듭니다.

@Autowired
public void init(EvenementPliRepository evenementPliRepository) 
{
    MyListenerClass.evenementPliRepository = evenementPliRepository;
    logger.info("Initializing with dependency ["+ evenementPliRepository +"]"); 
}

상세한 것에 대하여는, http://blog-en.lineofsightnet.com/2012/08/dependency-injection-on-stateless-beans.html 를 참조해 주세요.

이것은 사실 오래된 질문이지만 나는 다른 해결책을 찾았다.

public class MyEntityListener {
    @Autowired
    private ApplicationEventPublisher publisher;

    @PostPersist
    public void postPersist(MyEntity target) {
        SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);

        publisher.publishEvent(new OnCreatedEvent<>(this, target));
    }

    @PostUpdate
    public void postUpdate(MyEntity target) {
        SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);

        publisher.publishEvent(new OnUpdatedEvent<>(this, target));
    }

    @PostRemove
    public void postDelete(MyEntity target) {
        SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);

        publisher.publishEvent(new OnDeletedEvent<>(this, target));
    }
}

아마도 가장 좋은 것은 아니지만 AOP + 직조 기능이 없는 정적 변수보다 낫다.

리스너에게 @Component 주석을 달아 비 스태틱세터를 생성하여 주입된 Spring Bean을 할당하면 잘 작동합니다.

코드는 다음과 같습니다.

@Component
public class EntityListener {

    private static MyService service;

    @Autowired
    public void setMyService (MyService service) {
        this.service=service;
    }


    @PreUpdate
    public void onPreUpdate() {

        service.doThings()

    }

    @PrePersist
    public void onPersist() {
       ...
    }


}

Spring V5.1(및 휴지 상태 V5.3) 이후, Spring이 이러한 클래스의 프로바이더로서 등록되면, 곧바로 동작합니다.SpringBeanContainer 설명서 참조

이 솔루션은 어떻습니까?

@MappedSuperclass
@EntityListeners(AbstractEntityListener.class)
public abstract class AbstractEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;

    @Column(name = "creation_date")
    private Date creationDate;

    @Column(name = "modification_date")
    private Date modificationDate;

}

그러면 청취자는...

@Component
public class AbstractEntityListener {

    @Autowired
    private DateTimeService dateTimeService;

    @PreUpdate
    public void preUpdate(AbstractEntity abstractEntity) {
        AutowireHelper.autowire(this, this.dateTimeService);
            abstractEntity.setModificationDate(this.dateTimeService.getCurrentDate());
    }

    @PrePersist
    public void prePersist(AbstractEntity abstractEntity) {
        AutowireHelper.autowire(this, this.dateTimeService);
        Date currentDate = this.dateTimeService.getCurrentDate();
        abstractEntity.setCreationDate(currentDate);
        abstractEntity.setModificationDate(currentDate);
    }
}

그리고 도우미는...

    /**
     * Helper class which is able to autowire a specified class. It holds a static reference to the {@link org
     * .springframework.context.ApplicationContext}.
     */
    public final class AutowireHelper implements ApplicationContextAware {

        private static final AutowireHelper INSTANCE = new AutowireHelper();
        private static ApplicationContext applicationContext;

        private AutowireHelper() {
        }

        /**
         * Tries to autowire the specified instance of the class if one of the specified beans which need to be autowired
         * are null.
         *
         * @param classToAutowire the instance of the class which holds @Autowire annotations
         * @param beansToAutowireInClass the beans which have the @Autowire annotation in the specified {#classToAutowire}
         */
        public static void autowire(Object classToAutowire, Object... beansToAutowireInClass) {
            for (Object bean : beansToAutowireInClass) {
                if (bean == null) {
                    applicationContext.getAutowireCapableBeanFactory().autowireBean(classToAutowire);
                }
            }
        }

        @Override
        public void setApplicationContext(final ApplicationContext applicationContext) {
            AutowireHelper.applicationContext = applicationContext;
        }

        /**
         * @return the singleton instance.
         */
        public static AutowireHelper getInstance() {
            return INSTANCE;
        }

    }

저는 좋아요.

출처 : http://guylabs.ch/2014/02/22/autowiring-pring-beans-in-hibernate-jpa-entity-listeners/

저는 AOP를 사용하여 엔티티 청취자에게 스프링빈을 주입하는 길을 걷기 시작했습니다.하루 반 동안 조사를 하고 여러 가지 시도를 한 끝에 다음과 같은 링크를 발견했습니다.

스프링 관리 대상 콩을 JPA EntityListener 클래스에 주입할 수 없습니다.이는 JPA 리스너 메커니즘이 스테이트리스 클래스에 기반해야 하기 때문에 메서드는 효과적으로 스태틱하고 컨텍스트에 대응하지 않기 때문입니다.AOP의 양을 구하지 않고 리스너를 나타내는 '개체'에 아무것도 삽입되지 않습니다.실장에서는 실제로 인스턴스가 작성되지 않고 클래스 메서드가 사용되기 때문입니다.

이 시점에서 저는 다시 뭉쳐서 우연히 Eclipse Link Descriptor Event Adapter를 발견했습니다.이 정보를 사용하여 Descriptor Adapter를 확장하는 수신기 클래스를 만들었습니다.

public class EntityListener extends DescriptorEventAdapter {
    private String injectedValue;

    public void setInjectedValue(String value){
        this.injectedValue = value;
    }

    @Override
    public void aboutToInsert(DescriptorEvent event) {
       // Do what you need here
    }
}

클래스를 사용하려면 엔티티 클래스에 @EntityListeners 주석을 사용할 수 있습니다.유감스럽게도 이 방법으로는 Spring이 리스너 작성을 제어할 수 없기 때문에 의존관계 주입이 허용되지 않습니다.대신 다음 'init' 함수를 클래스에 추가했습니다.

public void init() {
    JpaEntityManager entityManager = null;

    try {
        // Create an entity manager for use in this function
        entityManager = (JpaEntityManager) entityManagerFactory.createEntityManager();
        // Use the entity manager to get a ClassDescriptor for the Entity class
        ClassDescriptor desc = 
            entityManager.getSession().getClassDescriptor(<EntityClass>.class);
        // Add this class as a listener to the class descriptor
        desc.getEventManager().addListener(this);
    } finally {
        if (entityManager != null) {
            // Cleanup the entity manager
            entityManager.close();
        }
    }
}

약간의 스프링 XML 구성 추가

<!-- Define listener object -->
<bean id="entityListener" class="EntityListener " init-method="init">
    <property name="injectedValue" value="Hello World"/>
    <property name="entityManagerFactory" ref="emf"/>
</bean>  

이제 Spring이 엔티티 리스너를 생성하여 필요한 의존관계를 주입하고 리스너 오브젝트가 리스닝 대상 엔티티 클래스에 자신을 등록하는 상황이 되었습니다.

이게 도움이 됐으면 좋겠어요.

ObjectFactory

@Configurable
public class YourEntityListener {
    @Autowired
    private ObjectFactory<YourBean> yourBeanProvider;

    @PrePersist
    public void beforePersist(Object target) {
       YourBean yourBean = yourBeanProvider.getObject();
       // do somthing with yourBean here
    }
}

는 이 을 는는이이에서 .org.springframework.data.jpa.domain.support.AuditingEntityListener - -jpa . spring - data - jpa 서 from from from 。

데모: https://github.com/eclipseAce/inject-into-entity-listener

저는 https://guylabs.ch/2014/02/22/autowiring-pring-beans-in-hibernate-jpa-entity-listeners/에서 제안하는 접근방식을 테스트하고 작업했습니다.아주 깨끗하진 않지만 제 역할을 한다.약간 변경된 자동 배선도우미 수업은 다음과 같습니다.

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class AutowireHelper implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    private AutowireHelper() {
    }

    public static void autowire(Object classToAutowire) {
        AutowireHelper.applicationContext.getAutowireCapableBeanFactory().autowireBean(classToAutowire);
    }

    @Override
    public void setApplicationContext(final ApplicationContext applicationContext) {
        AutowireHelper.applicationContext = applicationContext;
    }
}

그런 다음 엔티티 리스너에서 다음과 같이 호출합니다.

public class MyEntityAccessListener {

    @Autowired
    private MyService myService;


    @PostLoad
    public void postLoad(Object target) {

        AutowireHelper.autowire(this);

        myService.doThings();
        ...
    }

    public void setMyService(MyService myService) {
        this.myService = myService;
    }
}

JPA Listeners의 문제는 다음과 같습니다.

  1. 스프링에 의해 관리되지 않습니다(따라서 주입되지 않습니다).

  2. Spring의 Application Context가 준비되기 전에 생성(또는 생성될 수 있음)되므로 컨스트럭터 호출에 콩을 주입할 수 없습니다.

이 문제를 해결하기 위한 해결 방법:

작성 1) ★★Listener static을 하는 LISTENERS 추가:

public abstract class Listener {
    // for encapsulation purposes we have private modifiable and public non-modifiable lists
    private static final List<Listener> PRIVATE_LISTENERS = new ArrayList<>();
    public static final List<Listener> LISTENERS = Collections.unmodifiableList(PRIVATE_LISTENERS);

    protected Listener() {
        PRIVATE_LISTENERS.add(this);
    }
}

2) 2) JPA 리스너Listener.LISTENERS이 클래스를 .

public class MyListener extends Listener {

    @PrePersist
    public void onPersist() {
        ...
    }

    ...
}

3) Spring 어플리케이션 컨텍스트가 준비되면 청취자 전원과 콩을 주입할 수 있습니다.

@Component
public class ListenerInjector {

    @Autowired
    private ApplicationContext context;

    @EventListener(ContextRefreshedEvent.class)
    public void contextRefreshed() {
       Listener.LISTENERS.forEach(listener -> context.getAutowireCapableBeanFactory().autowireBean(listener));
    }

}

리스너 빈이 봄의 지배를 받지 못하기 때문인 것 같아요.봄은 그것을 인스턴스화하지 않는다, 봄은 어떻게 그 콩을 찾고 주사하는 방법을 알 수 있을까?

아직 시도하지 않았습니다만, 스프링의 설정 가능 주석이 있는 AspectJ Weaver를 사용하여 스프링이 아닌 원두를 제어할 수 있을 것 같습니다.

http://static.springsource.org/spring/docs/3.1.2.RELEASE/spring-framework-reference/html/aop.html#aop-using-aspectj

Hibernate 버전 5.3과 Spring 버전 5.1(Spring Boot 버전 2.1) 이후 간단한 해결책이 있습니다.해킹도, AOP도, 도우미 클래스도, 명시적 자동 배선도, 강제 주입을 위한 초기화 블록도 없습니다.

다음 작업만 하면 됩니다.

  1. 사람을 a로 하다@Component이치노
  2. Spring 어플리케이션에서 Spring을 콩 공급자로 사용하도록 JPA를 설정합니다.

(코틀린에서) 어떻게...

1) 기업청취자

@Component
class EntityXyzListener(val mySpringBean: MySpringBean) {

    @PostLoad
    fun afterLoad(entityXyz: EntityXyz) {
        // Injected bean is available here. (In my case the bean is a 
        // domain service that I make available to the entity.)
        entityXyz.mySpringBean= mySpringBean
    }

}

2) JPA 데이터 소스 구성

「 」에 .LocalContainerEntityManagerFactoryBean를 참조해 주세요. 여기에 더하면 요?jpaPropertyMap 키와 의 쌍:AvailableSettings.BEAN_CONTAINER=> 어플리케이션 컨텍스트의 콩 팩토리.

Spring Boot 어플리케이션에서는 데이터 소스를 설정하기 위한 다음 코드가 이미 준비되어 있습니다(예를 들어 여기에 있는 boilerplate 코드).코드 한 줄만 추가하면 됩니다.BEAN_CONTAINER의 속성jpaPropertyMap.

@Resource
lateinit var context: AbstractApplicationContext

@Primary
@Bean
@Qualifier("appDatasource")
@ConfigurationProperties(prefix = "spring.datasource")
fun myAppDatasource(): DataSource {
    return DataSourceBuilder.create().build()
}

@Primary
@Bean(name = ["myAppEntityManagerFactory"])
fun entityManagerFactoryBean(builder: EntityManagerFactoryBuilder): LocalContainerEntityManagerFactoryBean {
    val localContainerEntityManagerFactoryBean =
            builder
                    .dataSource(myAppDatasource())
                    .packages("com.mydomain.myapp")
                    .persistenceUnit("myAppPersistenceUnit")
                    .build()
    // the line below does the trick
    localContainerEntityManagerFactoryBean.jpaPropertyMap.put(
            AvailableSettings.BEAN_CONTAINER, SpringBeanContainer(context.beanFactory))
    return localContainerEntityManagerFactoryBean
}

다른 옵션:

Aplication Context에 액세스할 수 있도록 서비스를 만듭니다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;

import lombok.Setter;

@Service
class ContextWrapper {

    @Setter
    private static ApplicationContext context;

    @Autowired
    public ContextWrapper(ApplicationContext ac) {
        setContext(ac);
    }

    

}

사용방법:

...    
public class AuditListener {

    private static final String AUDIT_REPOSITORY = "AuditRepository";
    
    @PrePersist
    public void beforePersist(Object object){
        //TODO:
    }

    @PreUpdate
    public void beforeUpdate(Object object){
        //TODO:
    }
    
    @PreRemove
    public void beforeDelete(Object object) {
        getRepo().save(getAuditElement("DEL",object));
    }
    
    private Audit getAuditElement(String Operation,Object object){

        Audit audit = new Audit();
        audit.setActor("test");
        Timestamp timestamp = new Timestamp(System.currentTimeMillis());
        audit.setDate(timestamp);
        
        return audit;
    }

    private AuditRepository getRepo(){
        return ContextWrapper.getContext().getBean(AUDIT_REPOSITORY, AuditRepository.class);
    }
}

이 클래스는 jpa에서 청취자로 생성됩니다.

...
@Entity
@EntityListeners(AuditListener.class)
@NamedQuery(name="Customer.findAll", query="SELECT c FROM Customer c")
public class Customer implements Serializable {
    private static final long serialVersionUID = 1L;
...

청취자는 Spring의 통제를 받지 않기 때문에 컨텍스트 빈에 액세스할 수 없습니다.여러 옵션(@Configurable(...))을 시도했지만 컨텍스트에 대한 정적 액세스를 위한 클래스를 만드는 것 외에는 작동하지 않았습니다.이미 그 딜레마에 빠져있는 나는 이것이 우아한 선택이라고 생각한다.

가장 자연스러운 방법은 EntityListener 인스턴스화 프로세스에 개입하는 것이라고 생각합니다.이 방법은 5.3 이전 버전과 5.3 이후 버전에서 크게 다릅니다.

1) 5.3 이전 버전의 휴지 상태일 경우 org.hibernate.jpa.event.spi.jpa.ListenerFactory엔티티 리스트너의 인스턴스화는, 베이스의 CDI 를 제공하는 경우에 할 수 .javax.enterprise.inject.spi.BeanManagerCDI Spring DI(봄 DI) Spring Bean Factory(봄 콩 공장) CDI Bean(CDI 빈)입니다.

@Component
public class SpringCdiBeanManager implements BeanManager {

    @Autowired
    private BeanFactory beanFactory;

    @Override
    public <T> AnnotatedType<T> createAnnotatedType(Class<T> type) {
        return new SpringBeanType<T>(beanFactory, type);
    }

    @Override
    public <T> InjectionTarget<T> createInjectionTarget(AnnotatedType<T> type) {
       return (InjectionTarget<T>) type;
    }
    ...
    // have empty implementation for other methods 
}

타입 의존형 「」의 .SpringBeanType<T>다음과 같이 됩니다.

public class  SpringBeanType <T> implements AnnotatedType<T>, InjectionTarget<T>{

    private BeanFactory beanFactory;
    private Class<T> clazz;

    public SpringBeanType(BeanFactory beanFactory, Class<T> clazz) {
        this.beanFactory = beanFactory;
        this.clazz = clazz;
    }

    @Override
    public T produce(CreationalContext<T> ctx) {
        return beanFactory.getBean(clazz);
    }
    ...
    // have empty implementation for other methods 
}

Hibernate 에 Hibernate Configuration Settings(Hibernate Configuration Settings)(Hibernate Configuration Settings(Hibernate Configuration Settings)(Hibernate Configuration Settings)를.BeanManagerjavax.persistence.bean.manager 가지 방법이 있을 것입니다.그 중 한 아마도 여러 가지 방법이 있을 것입니다.그 중 하나를 가져오겠습니다.

@Configuration
public class HibernateConfig {

    @Autowired
    private SpringCdiBeanManager beanManager;

    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter(){
            @Override
            public Map<String, Object> getJpaPropertyMap(){
                Map<String, Object> jpaPropertyMap = super.getJpaPropertyMap();
                jpaPropertyMap.put("javax.persistence.bean.manager", beanManager);
                return jpaPropertyMap;
            }
        };
        // ...
        return jpaVendorAdapter;
    }
}

, 두 봄콩입니다.a)SpringCdiBeanManagerBeanFactory행 b) EntityListener를 할 수 .return beanFactory.getBean(clazz);성공할 것이다.

2) Hibernate 버전 5.3 이후에서는 @AdrianShum이 정확하게 지적한 바와 같이 Spring beans가 훨씬 쉬워졌습니다.5.3 휴지 상태에서는org.hibernate.resource.beans.container.spi.BeanContainerSpring Bean을 위한 즉시 사용할 수 있는 구현이 있습니다.org.springframework.orm.hibernate5.SpringBeanContainer이 경우 javadoc을 따릅니다.

Paulo Merson의 답변을 바탕으로 SpringBeanContainer를 설정하는 방법을 설명합니다.JpaBaseConfiguration다음은 두 가지 단계입니다.

1단계: 청취자를 스프링 컴포넌트로 정의합니다.자동 배선은 컨스트럭터 주입을 통해 작동합니다.

@Component
public class PliListener {

    private EvenementPliRepository evenementPliRepository;

    public PliListener(EvenementPliRepository repo) {
        this.evenementPliRepository = repo;
    }

    @PrePersist
    public void touchForCreate(Object target) {
        // ...
    }

    @PostPersist
    void onPostPersist(Object target) {
        // ...
    }
}

순서 2: 설정SpringBeanContainer를 사용하면, 리스너내의 자동 배선이 유효하게 됩니다.SpringBeanContainer JavaDoc은 볼만한 가치가 있습니다.

@Configuration
public class JpaConfig extends JpaBaseConfiguration {
    
    @Autowired
    private ConfigurableListableBeanFactory beanFactory;

    protected JpaConfig(DataSource dataSource, JpaProperties properties,
            ObjectProvider<JtaTransactionManager> jtaTransactionManager) {
        super(dataSource, properties, jtaTransactionManager);
    }

    @Override
    protected AbstractJpaVendorAdapter createJpaVendorAdapter() {
        return new HibernateJpaVendorAdapter();
    }

    @Override
    protected Map<String, Object> getVendorProperties() {
        Map<String, Object> props = new HashMap<>();

        // configure use of SpringBeanContainer
        props.put(org.hibernate.cfg.AvailableSettings.BEAN_CONTAINER, 
            new SpringBeanContainer(beanFactory));
        return props;
    }

}

다른 사람들이 지적했듯이 SpringBeanContainer는 Spring을 Hibernate의 ManagedBeanRegistryImpl에 연결하는 방법인 것 같습니다.Hibernate는 Hibernate가 콜백 오브젝트를 작성할 때 EntityListeners 인스턴스를 작성하는 역할을 합니다.콩을 작성하기 위한 콜은 SpringBeanContainer에 위임되며, SpringBeanContainer는 컨스트럭터 주입과 자동 배선을 모두 사용하여 스프링 콩을 작성할 수 있습니다.예를 들어 EntityListener는 다음과 같습니다.

public class MyEntityListener {

    @Autowired
    private AnotherBean anotherBean;

    private MyBean myBean;
    public InquiryEntityListener(MyBean myBean) {
        this.myBean = myBean;
    }

    public MyEntityListener() {
    }
}

EntityListener에서는 Hibernate에서 사용되지 않는 추가 인스턴스만 생성되므로 @Component 주석이 필요하지 않습니다.

단, SpringBeanContainer를 사용할 때는 몇 가지 중요한 제한사항과 주의사항이 있습니다.이 사용 예에서 EntityListener 인스턴스는 휴지 상태 EntityManager 생성 중에 생성되었습니다.이것은 봄의 라이프 사이클에서 매우 이른 시기에 발생했기 때문에, 이 시기에는 많은 콩이 존재하지 않았습니다.그 결과, 다음과 같은 검출이 가능하게 되었습니다.

SpringBeanContainer는 EntityListener 작성 시 존재하는 자동배선/컨스트럭터빈 의존관계만 수행합니다.생성자 종속성이 존재하지 않으면 기본 생성자가 호출됩니다.기본적으로 SpringBeanContainer를 사용할 때 레이스 조건이 있습니다.

회피책은 DefaultListableBeanFactory 인스턴스를 EntityListener에 주입하는 것입니다.나중에 EntityListeners 라이프 사이클 메서드가 호출되면(@PostLoad, @PostPersist 등), 원하는 빈 인스턴스를 BeanFactory에서 꺼낼 수 있습니다.이 시점에서 빈은 봄까지 작성되었을 것입니다.

언급URL : https://stackoverflow.com/questions/12155632/injecting-a-spring-dependency-into-a-jpa-entitylistener

반응형