복붙노트

[SPRING] 스프링 데이터 저장소에 맞춤 주석 조언 적용

SPRING

스프링 데이터 저장소에 맞춤 주석 조언 적용

mysql 마스터 슬레이브 복제 작업 중이다. 나는 spring data jpa (spring boot)를 사용하고있다.

필자가 필요로하는 모든 쓰기 작업은 마스터 서버 및 읽기 전용 작업으로 이동하여 여러 읽기 전용 슬레이브간에 똑같이 분산됩니다.

그것을 위해 나는해야한다 :

특수 JDBC 드라이버 사용 : com.mysql.jdbc.ReplicationDriver

복제에 설정 : URL에 :

spring:
    datasource:
        driverClassName: com.mysql.jdbc.ReplicationDriver
        url: jdbc:mysql:replication://127.0.0.1:3306,127.0.0.1:3307/MyForum?user=root&password=password&autoReconnect=true
        test-on-borrow: true
        validation-query: SELECT 1
    database: MYSQL

자동 커밋을 해제해야합니다. (*) 연결을 읽기 전용으로 설정해야합니다.

JDBC Connection이 읽기 전용으로 설정되도록하기 위해 주석과 간단한 AOP 인터셉터를 만들었습니다.

주석

package com.xyz.forum.replication;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; 

/**
 * Created by Bhupati Patel on 02/11/15.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ReadOnlyConnection {
}

요격기

package com.xyz.forum.replication;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.hibernate.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.persistence.EntityManager;

/**
 * Created by Bhupati Patel on 02/11/15.
 */

@Aspect
@Component
public class ConnectionInterceptor {

    private Logger logger;

    public ConnectionInterceptor() {
        logger = LoggerFactory.getLogger(getClass());
        logger.info("ConnectionInterceptor Started");
    }

    @Autowired
    private EntityManager entityManager;

    @Pointcut("@annotation(com.xyz.forum.replication.ReadOnlyConnection)")
    public void inReadOnlyConnection(){}


    @Around("inReadOnlyConnection()")
    public Object proceed(ProceedingJoinPoint pjp) throws Throwable {
        Session session = entityManager.unwrap(Session.class);
        ConnectionReadOnly readOnlyWork = new ConnectionReadOnly();

        try{
            session.doWork(readOnlyWork);
            return pjp.proceed();
        } finally {
            readOnlyWork.switchBack();
        }
    }

}

다음은 스프링 데이터 저장소입니다.

package com.xyz.forum.repositories;

import com.xyz.forum.entity.Topic;
import org.springframework.data.repository.Repository;

import java.util.List;

/**
 * Created by Bhupati Patel on 16/04/15.
 */
public interface TopicRepository extends Repository<Topic,Integer>{
    Topic save(Topic topic);
    Topic findByTopicIdAndIsDeletedFalse(Integer topicId);
    List<Topic> findByIsDeletedOrderByTopicOrderAsc(Boolean isDelete);

}

다음은 내 Manager (Service) 클래스입니다.

package com.xyz.forum.manager;

import com.xyz.forum.domain.entry.impl.TopicEntry;

import com.xyz.forum.domain.exception.impl.AuthException;

import com.xyz.forum.domain.exception.impl.NotFoundException;
import com.xyz.forum.entity.Topic;
import com.xyz.forum.replication.ReadOnlyConnection;
import com.xyz.forum.repositories.TopicRepository;
import com.xyz.forum.utils.converter.TopicConverter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;

/**
 * Created by Bhupati Patel on 16/04/15.
 */
@Repository
public class TopicManager {
    @Autowired
    TopicRepository topicRepository;

    @Transactional
    public TopicEntry save(TopicEntry topicEntry) {
        Topic topic = TopicConverter.fromEntryToEntity(topicEntry);
        return TopicConverter.fromEntityToEntry(topicRepository.save(topic));
    }

    @ReadOnlyConnection
    public TopicEntry get(Integer id) {
        Topic topicFromDb = topicRepository.findByTopicIdAndIsDeletedFalse(id);
        if(topicFromDb == null) {
            throw new NotFoundException("Invalid Id", "Topic Id [" + id + "] doesn't exist ");
        }
        return TopicConverter.fromEntityToEntry(topicFromDb);
    }
}

위의 코드에서 @ReadOnlyConnection 주석은 관리자 또는 서비스 계층에 지정되어 있습니다. 위의 코드 조각은 나를 위해 잘 작동합니다. 서비스 계층에서 슬레이브 db를 읽고 master db에 쓰는 것만으로도 사소한 경우입니다.

내 실제 요구 사항은 내가 읽기 / 쓰기 서비스 클래스의 다른 클래스에서 작업을 꽤 몇 가지 비즈니스 로직을 가지고 있기 때문에 저장소 수준 자체에서 @ReadOnlyConnection을 사용할 수 있어야한다고 했어. 그래서 난 @ ReadOnlyConnection을 넣을 수 없다. 서비스 계층.

나는 이런 것을 사용할 수 있어야한다.

public interface TopicRepository extends Repository<Topic,Integer>{
    Topic save(Topic topic);
    @ReadOnlyConnection
    Topic findByTopicIdAndIsDeletedFalse(Integer topicId);
    @ReadOnlyConnection
    List<Topic> findByIsDeletedOrderByTopicOrderAsc(Boolean isDelete);

}

스프링의 @Transactional 또는 @Modifying 또는 @Query 주석처럼. 다음은 제가 언급 한 예입니다.

    public interface AnswerRepository extends Repository<Answer,Integer> {
    @Transactional
    Answer save(Answer answer);

    @Transactional
    @Modifying
    @Query("update Answer ans set ans.isDeleted = 1, ans.deletedBy = :deletedBy, ans.deletedOn = :deletedOn " +
            "where ans.questionId = :questionId and ans.isDeleted = 0")
    void softDeleteBulkAnswers(@Param("deletedBy") String deletedBy, @Param("deletedOn") Date deletedOn,
                               @Param("questionId") Integer questionId);
}

나는 aspectj와 aop 세계에 초보자이다. 나는 ConnectionInterceptor에서 꽤 많은 pointcut regex를 시도했으나 그들 중 누구도 일하지 않았다. 나는 오랫동안 이걸 시도해 왔지만 아직 행운이 없다.

요청 된 작업을 달성하는 방법.

해결법

  1. ==============================

    1.메서드 수준에서 @ReadOnlyConnection (@Transactional과 같은)을 사용하는 사용자 지정 주석을 사용하는 방법을 얻을 수는 없었지만 작은 지옥이 나에게 효과적이었습니다.

    메서드 수준에서 @ReadOnlyConnection (@Transactional과 같은)을 사용하는 사용자 지정 주석을 사용하는 방법을 얻을 수는 없었지만 작은 지옥이 나에게 효과적이었습니다.

    아래에 코드 스 니펫을 붙여 넣습니다.

    @Aspect
    @Component
    @EnableAspectJAutoProxy
    public class ConnectionInterceptor {
    
        private Logger logger;
        private static final String JPA_PREFIX = "findBy";
        private static final String CUSTOM_PREFIX = "read";
    
        public ConnectionInterceptor() {
            logger = LoggerFactory.getLogger(getClass());
            logger.info("ConnectionInterceptor Started");
        }
    
        @Autowired
        private EntityManager entityManager;
    
        @Pointcut("this(org.springframework.data.repository.Repository)")
        public void inRepositoryLayer() {}
    
        @Around("inRepositoryLayer()")
        public Object proceed(ProceedingJoinPoint pjp) throws Throwable {
            String methodName = pjp.getSignature().getName();
            if (StringUtils.startsWith(methodName, JPA_PREFIX) || StringUtils.startsWith(methodName, CUSTOM_PREFIX)) {
                System.out.println("I'm there!" );
                Session session = entityManager.unwrap(Session.class);
                ConnectionReadOnly readOnlyWork = new ConnectionReadOnly();
    
                try{
                    session.doWork(readOnlyWork);
                    return pjp.proceed();
                } finally {
                    readOnlyWork.switchBack();
                }
            }
            return pjp.proceed();
        }
    }
    

    그래서 위의 코드에서 나는 다음과 같은 pointcut을 사용하고있다.

    @Pointcut("this(org.springframework.data.repository.Repository)")
    public void inRepositoryLayer() {}
    

    그게 무슨 일인가?

    당신은 그것을 볼 수있다. http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html

    이제 모든 저장소 읽기 쿼리 메서드는 위의 pointcut과 일치하는 내 메서드 실행에서 접두사 "findByXXX"(기본 spring-data-jpa 읽기 가능 메서드) 또는 "readXXX"(@Query 주석이있는 사용자 지정 읽기 메서드)로 시작합니다. 내 요구 사항에 따라 JDBC 연결 readOnly 사실 설정입니다.

    Session session = entityManager.unwrap(Session.class);
    ConnectionReadOnly readOnlyWork = new ConnectionReadOnly();
    

    그리고 내 ConnectionReadOnly는 다음과 같이 보입니다.

    package com.xyz.forum.replication;
    
    import org.hibernate.jdbc.Work;
    
    import java.sql.Connection;
    import java.sql.SQLException;
    
    /**
     * Created by Bhupati Patel on 04/11/15.
     */
    public class ConnectionReadOnly implements Work {
    
        private Connection connection;
        private boolean autoCommit;
        private boolean readOnly;
    
        @Override
        public void execute(Connection connection) throws SQLException {
            this.connection = connection;
            this.autoCommit = connection.getAutoCommit();
            this.readOnly = connection.isReadOnly();
            connection.setAutoCommit(false);
            connection.setReadOnly(true);
        }
    
        //method to restore the connection state before intercepted
        public void switchBack() throws SQLException{
            connection.setAutoCommit(autoCommit);
            connection.setReadOnly(readOnly);
        }
    }
    

    위의 설정이 내 요구 사항에 적용됩니다.

  2. from https://stackoverflow.com/questions/33527737/applying-custom-annotation-advice-to-spring-data-jpa-repository by cc-by-sa and MIT license