├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── java │ │ └── sh │ │ │ └── fable │ │ │ ├── book │ │ │ ├── BookRepository.java │ │ │ ├── BookService.java │ │ │ └── Book.java │ │ │ ├── persistence │ │ │ ├── routing │ │ │ │ └── RoutingDataSource.java │ │ │ ├── aop │ │ │ │ └── ReadOnlyRouteInterceptor.java │ │ │ └── DataSourceConfig.java │ │ │ └── Launcher.java │ └── resources │ │ └── application.yaml └── test │ └── java │ └── sh │ └── fable │ └── blog │ └── readrouter │ └── ReadRouterApplicationTests.java ├── docker-compose.yaml ├── README.md ├── sql └── init.sql ├── LICENSE ├── .gitignore ├── gradlew.bat └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'read-router' 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dominicgunn/fable-spring-read-replica/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Feb 06 12:27:20 CET 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-bin.zip 7 | -------------------------------------------------------------------------------- /src/main/java/sh/fable/book/BookRepository.java: -------------------------------------------------------------------------------- 1 | package sh.fable.book; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | /** 7 | * @author Dominic Gunn 8 | */ 9 | @Repository 10 | public interface BookRepository extends CrudRepository { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | postgres: 2 | image: postgres:latest 3 | ports: 4 | - "35432:5432" 5 | environment: 6 | - POSTGRES_DB=postgres 7 | - POSTGRES_USER=postgres 8 | - POSTGRES_PASSWORD=postgres 9 | volumes: 10 | - ./logs:/logs 11 | - ./sql:/docker-entrypoint-initdb.d 12 | command: postgres -c log_line_prefix="%a %u" -c log_statement=all -c logging_collector=true -c log_destination=stderr -c log_directory=/logs 13 | -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | primary: 3 | datasource: 4 | password: write 5 | username: fable_write 6 | driver: org.postgresql.Driver 7 | url: jdbc:postgresql://localhost:35432/postgres 8 | replica: 9 | datasource: 10 | password: read 11 | username: fable_read 12 | driver: org.postgresql.Driver 13 | url: jdbc:postgresql://localhost:35432/postgres 14 | 15 | # Spring Boot 2 + Hibernate Issue: https://hibernate.atlassian.net/browse/HHH-12368 16 | jpa: 17 | properties: 18 | hibernate: 19 | temp: 20 | use_jdbc_metadata_defaults: false 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Read/Write Routing Data Source 2 | 3 | This project aims to exemplify Springs ability to seperate read and write operations to your primary and replica databases. 4 | 5 | 6 | ## Read the blog 7 | 8 | A full blog post delving into details of this project is available for reading [here](https://fable.sh/blog/splitting-read-and-write-operations-in-spring-boot/). 9 | 10 | ## Usage 11 | 12 | In order to use this project, you'll need Docker and Java 8 installed. 13 | Run once: 14 | `mkdir ./logs; chmod 777 ./logs` 15 | Simply `docker-compose up` and then `./gradlew bootRun`. You should see logs of `fable_write` inserting data, and `fable_read` then reading. Easy! 16 | -------------------------------------------------------------------------------- /src/main/java/sh/fable/book/BookService.java: -------------------------------------------------------------------------------- 1 | package sh.fable.book; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.transaction.annotation.Transactional; 6 | 7 | /** 8 | * @author Dominic Gunn 9 | */ 10 | @Service 11 | public class BookService { 12 | 13 | @Autowired 14 | private BookRepository bookRepository; 15 | 16 | public Book save(final String author) { 17 | return bookRepository.save(new Book(author)); 18 | } 19 | 20 | @Transactional(readOnly = true) 21 | public Book get(final Integer id) { 22 | return bookRepository.findById(id).orElse(null); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/sh/fable/persistence/routing/RoutingDataSource.java: -------------------------------------------------------------------------------- 1 | package sh.fable.persistence.routing; 2 | 3 | import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; 4 | 5 | /** 6 | * @author Dominic Gunn 7 | */ 8 | public class RoutingDataSource extends AbstractRoutingDataSource { 9 | 10 | private static final ThreadLocal routeContext = new ThreadLocal<>(); 11 | 12 | public enum Route { 13 | PRIMARY, REPLICA 14 | } 15 | 16 | public static void clearReplicaRoute() { 17 | routeContext.remove(); 18 | } 19 | 20 | public static void setReplicaRoute() { 21 | routeContext.set(Route.REPLICA); 22 | } 23 | 24 | @Override 25 | public Object determineCurrentLookupKey() { 26 | return routeContext.get(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/sh/fable/Launcher.java: -------------------------------------------------------------------------------- 1 | package sh.fable; 2 | 3 | import sh.fable.book.BookService; 4 | 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.context.ApplicationContext; 8 | 9 | /** 10 | * @author Dominic Gunn 11 | */ 12 | @SpringBootApplication 13 | public class Launcher { 14 | 15 | public static void main(String[] args) { 16 | final ApplicationContext applicationContext = SpringApplication.run(Launcher.class, args); 17 | 18 | applicationContext.getBean(BookService.class).save("Test Author"); 19 | applicationContext.getBean(BookService.class).get(1); 20 | 21 | applicationContext.getBean(BookService.class).save("Test Author2"); 22 | applicationContext.getBean(BookService.class).get(2); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/sh/fable/book/Book.java: -------------------------------------------------------------------------------- 1 | package sh.fable.book; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import javax.persistence.Table; 9 | 10 | /** 11 | * @author Dominic Gunn 12 | */ 13 | @Entity 14 | @Table(name = "books") 15 | public class Book { 16 | 17 | @Id 18 | @Column(name = "id") 19 | @GeneratedValue(strategy = GenerationType.AUTO) 20 | private Integer id; 21 | 22 | @Column(name = "author", nullable = false) 23 | private String author; 24 | 25 | public Book() { 26 | 27 | } 28 | 29 | public Book(String author) { 30 | this.author = author; 31 | } 32 | 33 | public Integer getId() { 34 | return id; 35 | } 36 | 37 | public String getAuthor() { 38 | return author; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sql/init.sql: -------------------------------------------------------------------------------- 1 | -- Create our users. 2 | CREATE ROLE fable_read WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION PASSWORD 'md5010b66d00c67730d26587f13212502f4'; 3 | CREATE ROLE fable_write WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION PASSWORD 'md5e9b324e24230fe0105eb9d29dac29690'; 4 | 5 | -- Enable statement logging for our users. 6 | ALTER ROLE fable_read SET log_statement = 'all'; 7 | ALTER ROLE fable_write SET log_statement = 'all'; 8 | 9 | -- Create Books table. 10 | CREATE TABLE books ( 11 | id INTEGER NOT NULL, 12 | author VARCHAR(254) NOT NULL, 13 | CONSTRAINT pk_id PRIMARY KEY (id) 14 | ); 15 | 16 | -- Create grants for our users on the books table. 17 | GRANT SELECT ON TABLE books TO fable_read; 18 | GRANT SELECT, INSERT, DELETE, UPDATE ON TABLE books TO fable_write; 19 | 20 | -- Create a sequence for Hibernate. 21 | CREATE SEQUENCE hibernate_sequence START 1; 22 | 23 | -- Grant permissions to fable_write on the Hibernate Sequence. 24 | GRANT USAGE, SELECT ON SEQUENCE hibernate_sequence TO fable_write; 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dominic Gunn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/sh/fable/persistence/aop/ReadOnlyRouteInterceptor.java: -------------------------------------------------------------------------------- 1 | package sh.fable.persistence.aop; 2 | 3 | import org.aspectj.lang.ProceedingJoinPoint; 4 | import org.aspectj.lang.annotation.Around; 5 | import org.aspectj.lang.annotation.Aspect; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import sh.fable.persistence.routing.RoutingDataSource; 9 | 10 | import org.springframework.core.annotation.Order; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.transaction.annotation.Transactional; 13 | 14 | /** 15 | * @author Dominic Gunn 16 | */ 17 | @Aspect 18 | @Component 19 | @Order(0) 20 | public class ReadOnlyRouteInterceptor { 21 | 22 | private static final Logger logger = LoggerFactory.getLogger(ReadOnlyRouteInterceptor.class); 23 | 24 | @Around("@annotation(transactional)") 25 | public Object proceed(ProceedingJoinPoint proceedingJoinPoint, Transactional transactional) throws Throwable { 26 | try { 27 | if (transactional.readOnly()) { 28 | RoutingDataSource.setReplicaRoute(); 29 | logger.info("Routing database call to the read replica"); 30 | } 31 | return proceedingJoinPoint.proceed(); 32 | } finally { 33 | RoutingDataSource.clearReplicaRoute(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/sh/fable/persistence/DataSourceConfig.java: -------------------------------------------------------------------------------- 1 | package sh.fable.persistence; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import javax.sql.DataSource; 7 | 8 | import com.zaxxer.hikari.HikariConfig; 9 | import com.zaxxer.hikari.HikariDataSource; 10 | import sh.fable.persistence.routing.RoutingDataSource; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.Configuration; 15 | import org.springframework.context.annotation.Primary; 16 | import org.springframework.core.env.Environment; 17 | 18 | /** 19 | * @author Dominic Gunn 20 | */ 21 | @Configuration 22 | public class DataSourceConfig { 23 | 24 | private static final String PRIMARY_DATASOURCE_PREFIX = "spring.primary.datasource"; 25 | private static final String REPLICA_DATASOURCE_PREFIX = "spring.replica.datasource"; 26 | 27 | @Autowired 28 | private Environment environment; 29 | 30 | @Bean 31 | @Primary 32 | public DataSource dataSource() { 33 | final RoutingDataSource routingDataSource = new RoutingDataSource(); 34 | 35 | final DataSource primaryDataSource = buildDataSource("PrimaryHikariPool", PRIMARY_DATASOURCE_PREFIX); 36 | final DataSource replicaDataSource = buildDataSource("ReplicaHikariPool", REPLICA_DATASOURCE_PREFIX); 37 | 38 | final Map targetDataSources = new HashMap<>(); 39 | targetDataSources.put(RoutingDataSource.Route.PRIMARY, primaryDataSource); 40 | targetDataSources.put(RoutingDataSource.Route.REPLICA, replicaDataSource); 41 | 42 | routingDataSource.setTargetDataSources(targetDataSources); 43 | routingDataSource.setDefaultTargetDataSource(primaryDataSource); 44 | 45 | return routingDataSource; 46 | } 47 | 48 | private DataSource buildDataSource(String poolName, String dataSourcePrefix) { 49 | final HikariConfig hikariConfig = new HikariConfig(); 50 | 51 | hikariConfig.setPoolName(poolName); 52 | hikariConfig.setJdbcUrl(environment.getProperty(String.format("%s.url", dataSourcePrefix))); 53 | hikariConfig.setUsername(environment.getProperty(String.format("%s.username", dataSourcePrefix))); 54 | hikariConfig.setPassword(environment.getProperty(String.format("%s.password", dataSourcePrefix))); 55 | hikariConfig.setDriverClassName(environment.getProperty(String.format("%s.driver", dataSourcePrefix))); 56 | 57 | return new HikariDataSource(hikariConfig); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/maven,gradle,intellij 3 | 4 | ### Intellij ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff: 9 | .idea/** 10 | *.iml 11 | 12 | # Sensitive or high-churn files: 13 | .idea/**/dataSources/ 14 | .idea/**/dataSources.ids 15 | .idea/**/dataSources.xml 16 | .idea/**/dataSources.local.xml 17 | .idea/**/sqlDataSources.xml 18 | .idea/**/dynamic.xml 19 | .idea/**/uiDesigner.xml 20 | 21 | # Gradle: 22 | .idea/**/gradle.xml 23 | .idea/**/libraries 24 | 25 | # CMake 26 | cmake-build-debug/ 27 | 28 | # Mongo Explorer plugin: 29 | .idea/**/mongoSettings.xml 30 | 31 | ## File-based project format: 32 | *.iws 33 | 34 | ## Plugin-specific files: 35 | 36 | # IntelliJ 37 | /out/ 38 | 39 | # mpeltonen/sbt-idea plugin 40 | .idea_modules/ 41 | 42 | # JIRA plugin 43 | atlassian-ide-plugin.xml 44 | 45 | # Cursive Clojure plugin 46 | .idea/replstate.xml 47 | 48 | # Ruby plugin and RubyMine 49 | /.rakeTasks 50 | 51 | # Crashlytics plugin (for Android Studio and IntelliJ) 52 | com_crashlytics_export_strings.xml 53 | crashlytics.properties 54 | crashlytics-build.properties 55 | fabric.properties 56 | 57 | ### Intellij Patch ### 58 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 59 | 60 | # *.iml 61 | # modules.xml 62 | # .idea/misc.xml 63 | # *.ipr 64 | 65 | # Sonarlint plugin 66 | .idea/sonarlint 67 | 68 | ### Maven ### 69 | target/ 70 | pom.xml.tag 71 | pom.xml.releaseBackup 72 | pom.xml.versionsBackup 73 | pom.xml.next 74 | release.properties 75 | dependency-reduced-pom.xml 76 | buildNumber.properties 77 | .mvn/timing.properties 78 | 79 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 80 | !/.mvn/wrapper/maven-wrapper.jar 81 | 82 | ### Gradle ### 83 | .gradle 84 | **/build/ 85 | 86 | # Ignore Gradle GUI config 87 | gradle-app.setting 88 | 89 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 90 | !gradle-wrapper.jar 91 | 92 | # Cache of project 93 | .gradletasknamecache 94 | 95 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 96 | # gradle/wrapper/gradle-wrapper.properties 97 | 98 | 99 | # End of https://www.gitignore.io/api/maven,gradle,intellij 100 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/test/java/sh/fable/blog/readrouter/ReadRouterApplicationTests.java: -------------------------------------------------------------------------------- 1 | package sh.fable.blog.readrouter; 2 | 3 | import static org.junit.Assert.*; 4 | import static org.mockito.Mockito.*; 5 | 6 | import org.aspectj.lang.ProceedingJoinPoint; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | import javax.sql.DataSource; 16 | import sh.fable.persistence.aop.ReadOnlyRouteInterceptor; 17 | import sh.fable.persistence.routing.RoutingDataSource; 18 | 19 | @RunWith(SpringRunner.class) 20 | @SpringBootTest 21 | public class ReadRouterApplicationTests { 22 | 23 | @Autowired 24 | private DataSource dataSource; 25 | private ReadOnlyRouteInterceptor readOnlyRouteInterceptor; 26 | private Transactional transactionalMock; 27 | private ProceedingJoinPoint proceedingJoinPointMock; 28 | private RoutingDataSource routingDataSource; 29 | 30 | @Before 31 | public void setup() { 32 | readOnlyRouteInterceptor = new ReadOnlyRouteInterceptor(); 33 | transactionalMock = mock(Transactional.class); 34 | proceedingJoinPointMock = mock(ProceedingJoinPoint.class); 35 | routingDataSource = (RoutingDataSource) dataSource; 36 | } 37 | 38 | @Test 39 | public void replicaRoutingIsPossible() throws Throwable { 40 | when(transactionalMock.readOnly()).thenReturn(true); 41 | when(proceedingJoinPointMock.proceed()).then(invocation -> { 42 | assertEquals(RoutingDataSource.Route.REPLICA, routingDataSource.determineCurrentLookupKey()); 43 | return null; 44 | }); 45 | readOnlyRouteInterceptor.proceed(proceedingJoinPointMock, transactionalMock); 46 | } 47 | 48 | @Test 49 | public void defaultRoutingIsPossible() throws Throwable { 50 | when(transactionalMock.readOnly()).thenReturn(false); 51 | when(proceedingJoinPointMock.proceed()).then(invocation -> { 52 | assertNull(routingDataSource.determineCurrentLookupKey()); 53 | return null; 54 | }); 55 | readOnlyRouteInterceptor.proceed(proceedingJoinPointMock, transactionalMock); 56 | } 57 | 58 | @Test 59 | public void dataSourceIsClearedAgain() throws Throwable { 60 | RoutingDataSource.setReplicaRoute(); 61 | assertNotNull(routingDataSource.determineCurrentLookupKey()); 62 | readOnlyRouteInterceptor.proceed(proceedingJoinPointMock, transactionalMock); 63 | assertNull(routingDataSource.determineCurrentLookupKey()); 64 | } 65 | 66 | @Test 67 | public void contextLoads() { 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | --------------------------------------------------------------------------------