2015-11-04 68 views
2

我正在開發mysql主從複製。我使用彈簧數據jpa(彈簧引導)。將自定義註釋建議應用於彈簧數據jpa存儲庫

我需要的是所有寫入操作去主服務器和只讀操作平等分佈在多個只讀從站。

對於我需要:

使用特殊的JDBC驅動程序:com.mysql.jdbc.ReplicationDriver

設置複製:在網址:

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連接設置爲只讀,我創建了一個註釋和一個簡單的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); 

} 

以下是我的經理(服務)類。

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註釋是在管理器或服務層中指定的。 以上幾段代碼對我來說工作得很好。這是一個微不足道的情況,在服務層,我只從slave 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); 

} 

像Spring的@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幾個切入點正則表達式,但沒有一次成功。我很長一段時間以來一直在嘗試,但沒有成功。

如何實現問題任務。

+0

您是否已將'@ EnableAspectJAutoProxy'添加到您的配置中? – px5x2

+0

是的。我做到了。我提到「上面的代碼對我來說工作正常」。我沒有把所有的代碼放在這裏。我只放了一段重要的代碼片段。 –

+0

嘿Bhupati,你是否能夠實現你的目標在你的存儲庫類中指定@Transactional Annotation?如果是,你做了什麼改變。你可以在這裏發佈解決方案來幫助其他人(比如我:P)。謝謝 –

回答

3

我無法在方法級別獲得我的自定義註釋@ReadOnlyConnection(如@Transactional)的解決方法,但小小的heck確實爲我工作。

我在粘貼下面的代碼片段。

@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("this(org.springframework.data.repository.Repository)") 
public void inRepositoryLayer() {} 

和它的作用是

切入點任意連接點(方法執行只在Spring AOP),其中代理實現版本庫界面

你可以看看它在 http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html

現在我所有的存儲庫中讀取查詢方法或者這在我的身邊方法有前綴「findByXXX」(默認彈簧數據的JPA讀法)或「的readXXX」(自定義閱讀方法與@Query註解)啓動執行與上述切入點匹配。根據我的要求,我設置了JDBC連接readOnly爲true。

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); 
    } 
} 

所以上面的設置爲我的要求工作。