├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jitpack.yml ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main └── java │ └── net │ └── ttddyy │ └── dsproxy │ └── r2dbc │ ├── ProxyConnectionFactoryBuilder.java │ ├── core │ ├── Binding.java │ ├── BindingValue.java │ ├── Bindings.java │ ├── CompositeProxyExecutionListener.java │ ├── ConnectionHolder.java │ ├── ConnectionIdManager.java │ ├── ConnectionInfo.java │ ├── DefaultConnectionIdManager.java │ ├── ExecutionType.java │ ├── MethodExecutionInfo.java │ ├── ProxyEventType.java │ ├── ProxyExecutionListener.java │ ├── QueryExecutionInfo.java │ └── QueryInfo.java │ ├── proxy │ ├── CallbackSupport.java │ ├── JdkProxyFactory.java │ ├── ProxyConfig.java │ ├── ProxyFactory.java │ ├── ProxyObject.java │ ├── ProxyUtils.java │ ├── ReactiveBatchCallback.java │ ├── ReactiveConnectionCallback.java │ ├── ReactiveConnectionFactoryCallback.java │ ├── ReactiveResultCallback.java │ └── ReactiveStatementCallback.java │ └── support │ ├── LastExecutionAwareListener.java │ ├── LifeCycleExecutionListener.java │ ├── LifeCycleListener.java │ ├── MethodExecutionInfoFormatter.java │ └── QueryExecutionInfoFormatter.java └── test ├── java └── net │ └── ttddyy │ └── dsproxy │ └── r2dbc │ ├── Example.java │ ├── PostgresqlExample.java │ ├── core │ └── CompositeProxyExecutionListenerTest.java │ ├── proxy │ ├── CallbackSupportTest.java │ ├── JdkProxyFactoryTest.java │ ├── ProxyUtilsTest.java │ ├── ReactiveBatchCallbackTest.java │ ├── ReactiveConnectionCallbackTest.java │ ├── ReactiveConnectionFactoryCallbackTest.java │ ├── ReactiveResultCallbackTest.java │ └── ReactiveStatementCallbackTest.java │ └── support │ ├── LifeCycleExecutionListenerTest.java │ ├── LifeCycleListenerTest.java │ ├── MethodExecutionInfoFormatterTest.java │ ├── ProxyClassesSource.java │ └── QueryExecutionInfoFormatterTest.java └── resources └── logback.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | *.ipr 4 | *.iws 5 | .idea/ 6 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | import java.net.*; 21 | import java.io.*; 22 | import java.nio.channels.*; 23 | import java.util.Properties; 24 | 25 | public class MavenWrapperDownloader { 26 | 27 | /** 28 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 29 | */ 30 | private static final String DEFAULT_DOWNLOAD_URL = 31 | "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; 32 | 33 | /** 34 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 35 | * use instead of the default one. 36 | */ 37 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 38 | ".mvn/wrapper/maven-wrapper.properties"; 39 | 40 | /** 41 | * Path where the maven-wrapper.jar will be saved to. 42 | */ 43 | private static final String MAVEN_WRAPPER_JAR_PATH = 44 | ".mvn/wrapper/maven-wrapper.jar"; 45 | 46 | /** 47 | * Name of the property which should be used to override the default download url for the wrapper. 48 | */ 49 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 50 | 51 | public static void main(String args[]) { 52 | System.out.println("- Downloader started"); 53 | File baseDirectory = new File(args[0]); 54 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 55 | 56 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 57 | // wrapperUrl parameter. 58 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 59 | String url = DEFAULT_DOWNLOAD_URL; 60 | if(mavenWrapperPropertyFile.exists()) { 61 | FileInputStream mavenWrapperPropertyFileInputStream = null; 62 | try { 63 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 64 | Properties mavenWrapperProperties = new Properties(); 65 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 66 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 67 | } catch (IOException e) { 68 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 69 | } finally { 70 | try { 71 | if(mavenWrapperPropertyFileInputStream != null) { 72 | mavenWrapperPropertyFileInputStream.close(); 73 | } 74 | } catch (IOException e) { 75 | // Ignore ... 76 | } 77 | } 78 | } 79 | System.out.println("- Downloading from: : " + url); 80 | 81 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 82 | if(!outputFile.getParentFile().exists()) { 83 | if(!outputFile.getParentFile().mkdirs()) { 84 | System.out.println( 85 | "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 86 | } 87 | } 88 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 89 | try { 90 | downloadFileFromURL(url, outputFile); 91 | System.out.println("Done"); 92 | System.exit(0); 93 | } catch (Throwable e) { 94 | System.out.println("- Error downloading"); 95 | e.printStackTrace(); 96 | System.exit(1); 97 | } 98 | } 99 | 100 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 101 | URL website = new URL(urlString); 102 | ReadableByteChannel rbc; 103 | rbc = Channels.newChannel(website.openStream()); 104 | FileOutputStream fos = new FileOutputStream(destination); 105 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 106 | fos.close(); 107 | rbc.close(); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttddyy/datasource-proxy-r2dbc/fa885a0afbf9b0baacd06c7e3247808466c515fe/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | cache: 7 | directories: 8 | - ~/.m2/repository 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # v0.2 3 | 4 | * Proxy `Result` class. 5 | 6 | * Update `ProxyExecutionListener#eachQueryResult()` implementation. 7 | 8 | * Add `[before|after]Method()`, `[before|after]Query()` methods on `LifeCycleListener` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | # 2 | # config for https://jitpack.io 3 | # 4 | jdk: 5 | - oraclejdk8 -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | # TODO classpath? 118 | fi 119 | 120 | if [ -z "$JAVA_HOME" ]; then 121 | javaExecutable="`which javac`" 122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 123 | # readlink(1) is not available as standard on Solaris 10. 124 | readLink=`which readlink` 125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 126 | if $darwin ; then 127 | javaHome="`dirname \"$javaExecutable\"`" 128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 129 | else 130 | javaExecutable="`readlink -f \"$javaExecutable\"`" 131 | fi 132 | javaHome="`dirname \"$javaExecutable\"`" 133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 134 | JAVA_HOME="$javaHome" 135 | export JAVA_HOME 136 | fi 137 | fi 138 | fi 139 | 140 | if [ -z "$JAVACMD" ] ; then 141 | if [ -n "$JAVA_HOME" ] ; then 142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 143 | # IBM's JDK on AIX uses strange locations for the executables 144 | JAVACMD="$JAVA_HOME/jre/sh/java" 145 | else 146 | JAVACMD="$JAVA_HOME/bin/java" 147 | fi 148 | else 149 | JAVACMD="`which java`" 150 | fi 151 | fi 152 | 153 | if [ ! -x "$JAVACMD" ] ; then 154 | echo "Error: JAVA_HOME is not defined correctly." >&2 155 | echo " We cannot execute $JAVACMD" >&2 156 | exit 1 157 | fi 158 | 159 | if [ -z "$JAVA_HOME" ] ; then 160 | echo "Warning: JAVA_HOME environment variable is not set." 161 | fi 162 | 163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 164 | 165 | # traverses directory structure from process work directory to filesystem root 166 | # first directory with .mvn subdirectory is considered project base directory 167 | find_maven_basedir() { 168 | 169 | if [ -z "$1" ] 170 | then 171 | echo "Path not specified to find_maven_basedir" 172 | return 1 173 | fi 174 | 175 | basedir="$1" 176 | wdir="$1" 177 | while [ "$wdir" != '/' ] ; do 178 | if [ -d "$wdir"/.mvn ] ; then 179 | basedir=$wdir 180 | break 181 | fi 182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 183 | if [ -d "${wdir}" ]; then 184 | wdir=`cd "$wdir/.."; pwd` 185 | fi 186 | # end of workaround 187 | done 188 | echo "${basedir}" 189 | } 190 | 191 | # concatenates all lines of a file 192 | concat_lines() { 193 | if [ -f "$1" ]; then 194 | echo "$(tr -s '\n' ' ' < "$1")" 195 | fi 196 | } 197 | 198 | BASE_DIR=`find_maven_basedir "$(pwd)"` 199 | if [ -z "$BASE_DIR" ]; then 200 | exit 1; 201 | fi 202 | 203 | ########################################################################################## 204 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 205 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 206 | ########################################################################################## 207 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found .mvn/wrapper/maven-wrapper.jar" 210 | fi 211 | else 212 | if [ "$MVNW_VERBOSE" = true ]; then 213 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 214 | fi 215 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 216 | while IFS="=" read key value; do 217 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 218 | esac 219 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Downloading from: $jarUrl" 222 | fi 223 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 224 | 225 | if command -v wget > /dev/null; then 226 | if [ "$MVNW_VERBOSE" = true ]; then 227 | echo "Found wget ... using wget" 228 | fi 229 | wget "$jarUrl" -O "$wrapperJarPath" 230 | elif command -v curl > /dev/null; then 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Found curl ... using curl" 233 | fi 234 | curl -o "$wrapperJarPath" "$jarUrl" 235 | else 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Falling back to using Java to download" 238 | fi 239 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 240 | if [ -e "$javaClass" ]; then 241 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 242 | if [ "$MVNW_VERBOSE" = true ]; then 243 | echo " - Compiling MavenWrapperDownloader.java ..." 244 | fi 245 | # Compiling the Java class 246 | ("$JAVA_HOME/bin/javac" "$javaClass") 247 | fi 248 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 249 | # Running the downloader 250 | if [ "$MVNW_VERBOSE" = true ]; then 251 | echo " - Running MavenWrapperDownloader.java ..." 252 | fi 253 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 254 | fi 255 | fi 256 | fi 257 | fi 258 | ########################################################################################## 259 | # End of extension 260 | ########################################################################################## 261 | 262 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 263 | if [ "$MVNW_VERBOSE" = true ]; then 264 | echo $MAVEN_PROJECTBASEDIR 265 | fi 266 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 267 | 268 | # For Cygwin, switch paths to Windows format before running java 269 | if $cygwin; then 270 | [ -n "$M2_HOME" ] && 271 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 272 | [ -n "$JAVA_HOME" ] && 273 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 274 | [ -n "$CLASSPATH" ] && 275 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 276 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 277 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 278 | fi 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 285 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 286 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 287 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 124 | FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( 125 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | echo Found %WRAPPER_JAR% 132 | ) else ( 133 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 134 | echo Downloading from: %DOWNLOAD_URL% 135 | powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" 136 | echo Finished downloading %WRAPPER_JAR% 137 | ) 138 | @REM End of extension 139 | 140 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 141 | if ERRORLEVEL 1 goto error 142 | goto end 143 | 144 | :error 145 | set ERROR_CODE=1 146 | 147 | :end 148 | @endlocal & set ERROR_CODE=%ERROR_CODE% 149 | 150 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 151 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 152 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 153 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 154 | :skipRcPost 155 | 156 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 157 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 158 | 159 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 160 | 161 | exit /B %ERROR_CODE% 162 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | net.ttddyy 5 | datasource-proxy-r2dbc 6 | 0.3-SNAPSHOT 7 | jar 8 | 9 | datasource-proxy-r2dbc 10 | datasource-proxy for r2dbc 11 | https://github.com/ttddyy/datasource-proxy-r2dbc/ 12 | 13 | 14 | 15 | Apache License, Version 2.0 16 | https://www.apache.org/licenses/LICENSE-2.0.txt 17 | repo 18 | 19 | 20 | 21 | 22 | scm:git:git@github.com:ttddyy/datasource-proxy-r2dbc.git 23 | scm:git:git@github.com:ttddyy/datasource-proxy-r2dbc.git 24 | git@github.com:ttddyy/datasource-proxy-r2dbc.git 25 | HEAD 26 | 27 | 28 | 29 | 30 | bintray 31 | https://api.bintray.com/maven/ttddyy/maven/datasource-proxy-r2dbc 32 | 33 | 34 | ossrh 35 | https://oss.sonatype.org/content/repositories/snapshots 36 | 37 | 38 | 39 | 40 | 41 | tadaya 42 | Tadaya Tsuyukubo 43 | tadaya@ttddyy.net 44 | http://www.ttddyy.net 45 | 46 | 47 | 48 | 49 | 50 | UTF-8 51 | 52 | 1.8 53 | 54 | 55 | 3.8.0 56 | 2.22.1 57 | 3.0.1 58 | 3.0.1 59 | 2.5.3 60 | 61 | 62 | 1.0.0.M6 63 | Californium-SR2 64 | 65 | 66 | 1.0.0.M6 67 | 1.0.0.M6 68 | 69 | 5.3.2 70 | 2.23.0 71 | 3.11.1 72 | 1.2.3 73 | 1.9.1 74 | 2.1.0.RELEASE 75 | 42.2.5 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | io.projectreactor 86 | reactor-bom 87 | ${reactor.version} 88 | pom 89 | import 90 | 91 | 92 | 93 | org.junit 94 | junit-bom 95 | ${junit.jupiter.version} 96 | pom 97 | import 98 | 99 | 100 | 101 | org.testcontainers 102 | testcontainers-bom 103 | ${testcontainers.version} 104 | pom 105 | import 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | org.apache.maven.plugins 116 | maven-compiler-plugin 117 | 3.8.0 118 | 119 | ${java.version} 120 | ${java.version} 121 | ${java.version} 122 | ${java.version} 123 | 124 | 125 | 126 | 127 | org.apache.maven.plugins 128 | maven-surefire-plugin 129 | ${surefire.version} 130 | 131 | random 132 | 133 | 134 | 135 | 136 | org.apache.maven.plugins 137 | maven-source-plugin 138 | ${source.version} 139 | 140 | 141 | attach-sources 142 | 143 | jar 144 | 145 | 146 | 147 | 148 | 149 | 150 | org.apache.maven.plugins 151 | maven-javadoc-plugin 152 | ${javadoc.version} 153 | 154 | 155 | attach-javadocs 156 | 157 | jar 158 | 159 | 160 | 161 | 162 | 163 | none 164 | 165 | 166 | 167 | 168 | org.apache.maven.plugins 169 | maven-release-plugin 170 | ${release.version} 171 | 172 | 173 | org.apache.maven.scm 174 | maven-scm-provider-gitexe 175 | 1.9.5 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | spring-snapshots 186 | Spring Snapshots 187 | https://repo.spring.io/snapshot 188 | 189 | true 190 | 191 | 192 | 193 | spring-milestones 194 | Spring Milestones 195 | https://repo.spring.io/milestone 196 | 197 | false 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | io.r2dbc 207 | r2dbc-spi 208 | ${r2dbc-spi.version} 209 | 210 | 211 | io.projectreactor 212 | reactor-core 213 | 214 | 215 | 216 | 217 | 218 | io.r2dbc 219 | r2dbc-postgresql 220 | ${r2dbc-postgresql.version} 221 | test 222 | 223 | 224 | io.r2dbc 225 | r2dbc-client 226 | ${r2dbc-client.version} 227 | test 228 | 229 | 230 | 231 | io.projectreactor 232 | reactor-test 233 | test 234 | 235 | 236 | 237 | org.mockito 238 | mockito-core 239 | ${mockito.version} 240 | test 241 | 242 | 243 | org.mockito 244 | mockito-junit-jupiter 245 | ${mockito.version} 246 | test 247 | 248 | 249 | 250 | org.assertj 251 | assertj-core 252 | ${assertj.version} 253 | test 254 | 255 | 256 | 257 | ch.qos.logback 258 | logback-classic 259 | ${logback.versino} 260 | test 261 | 262 | 263 | 264 | org.junit.jupiter 265 | junit-jupiter-api 266 | test 267 | 268 | 269 | org.junit.jupiter 270 | junit-jupiter-params 271 | test 272 | 273 | 274 | org.junit.jupiter 275 | junit-jupiter-engine 276 | test 277 | 278 | 279 | 280 | org.springframework.boot 281 | spring-boot-starter-jdbc 282 | ${spring-boot.version} 283 | test 284 | 285 | 286 | 287 | org.testcontainers 288 | postgresql 289 | test 290 | 291 | 292 | 293 | org.postgresql 294 | postgresql 295 | ${postgresql.version} 296 | test 297 | 298 | 299 | 300 | 301 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/ProxyConnectionFactoryBuilder.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc; 2 | 3 | import io.r2dbc.spi.ConnectionFactory; 4 | import net.ttddyy.dsproxy.r2dbc.core.MethodExecutionInfo; 5 | import net.ttddyy.dsproxy.r2dbc.core.ProxyExecutionListener; 6 | import net.ttddyy.dsproxy.r2dbc.core.QueryExecutionInfo; 7 | import net.ttddyy.dsproxy.r2dbc.proxy.ProxyConfig; 8 | import net.ttddyy.dsproxy.r2dbc.support.LifeCycleExecutionListener; 9 | import net.ttddyy.dsproxy.r2dbc.support.LifeCycleListener; 10 | import reactor.core.publisher.Mono; 11 | 12 | import java.util.Objects; 13 | import java.util.function.Consumer; 14 | 15 | /** 16 | * @author Tadaya Tsuyukubo 17 | */ 18 | public class ProxyConnectionFactoryBuilder { 19 | 20 | private ConnectionFactory delegate; 21 | 22 | private ProxyConfig proxyConfig = new ProxyConfig(); // default 23 | 24 | public ProxyConnectionFactoryBuilder(ConnectionFactory delegate) { 25 | this.delegate = delegate; 26 | } 27 | 28 | public static ProxyConnectionFactoryBuilder create(ConnectionFactory delegate) { 29 | Objects.requireNonNull(delegate, "ConnectionFactory to delegate is required"); 30 | return new ProxyConnectionFactoryBuilder(delegate); 31 | } 32 | 33 | public static ProxyConnectionFactoryBuilder create(ConnectionFactory delegate, ProxyConfig proxyConfig) { 34 | return create(delegate).proxyConfig(proxyConfig); 35 | } 36 | 37 | public ConnectionFactory build() { 38 | return this.proxyConfig.getProxyFactory().createConnectionFactory(this.delegate); 39 | } 40 | 41 | 42 | public ProxyConnectionFactoryBuilder proxyConfig(ProxyConfig proxyConfig) { 43 | this.proxyConfig = proxyConfig; 44 | return this; 45 | } 46 | 47 | public ProxyConnectionFactoryBuilder onMethodExecution(Consumer> consumer) { 48 | this.proxyConfig.addListener(new ProxyExecutionListener() { 49 | @Override 50 | public void onMethodExecution(MethodExecutionInfo executionInfo) { 51 | consumer.accept(Mono.just(executionInfo)); 52 | } 53 | }); 54 | return this; 55 | } 56 | 57 | public ProxyConnectionFactoryBuilder onQueryExecution(Consumer> consumer) { 58 | this.proxyConfig.addListener(new ProxyExecutionListener() { 59 | @Override 60 | public void onQueryExecution(QueryExecutionInfo executionInfo) { 61 | consumer.accept(Mono.just(executionInfo)); 62 | } 63 | }); 64 | return this; 65 | } 66 | 67 | public ProxyConnectionFactoryBuilder onBeforeMethod(Consumer> consumer) { 68 | this.proxyConfig.addListener(new ProxyExecutionListener() { 69 | @Override 70 | public void beforeMethod(MethodExecutionInfo executionInfo) { 71 | consumer.accept(Mono.just(executionInfo)); 72 | } 73 | }); 74 | return this; 75 | } 76 | 77 | public ProxyConnectionFactoryBuilder onAfterMethod(Consumer> consumer) { 78 | this.proxyConfig.addListener(new ProxyExecutionListener() { 79 | @Override 80 | public void afterMethod(MethodExecutionInfo executionInfo) { 81 | consumer.accept(Mono.just(executionInfo)); 82 | } 83 | }); 84 | return this; 85 | } 86 | 87 | public ProxyConnectionFactoryBuilder onBeforeQuery(Consumer> consumer) { 88 | this.proxyConfig.addListener(new ProxyExecutionListener() { 89 | @Override 90 | public void beforeQuery(QueryExecutionInfo executionInfo) { 91 | consumer.accept(Mono.just(executionInfo)); 92 | } 93 | }); 94 | return this; 95 | } 96 | 97 | public ProxyConnectionFactoryBuilder onAfterQuery(Consumer> consumer) { 98 | this.proxyConfig.addListener(new ProxyExecutionListener() { 99 | @Override 100 | public void afterQuery(QueryExecutionInfo executionInfo) { 101 | consumer.accept(Mono.just(executionInfo)); 102 | } 103 | }); 104 | return this; 105 | } 106 | 107 | public ProxyConnectionFactoryBuilder onEachQueryResult(Consumer> consumer) { 108 | this.proxyConfig.addListener(new ProxyExecutionListener() { 109 | @Override 110 | public void eachQueryResult(QueryExecutionInfo executionInfo) { 111 | consumer.accept(Mono.just(executionInfo)); 112 | } 113 | }); 114 | return this; 115 | } 116 | 117 | public ProxyConnectionFactoryBuilder listener(ProxyExecutionListener listener) { 118 | this.proxyConfig.addListener(listener); 119 | return this; 120 | } 121 | 122 | public ProxyConnectionFactoryBuilder listener(LifeCycleListener lifeCycleListener) { 123 | this.listener(LifeCycleExecutionListener.of(lifeCycleListener)); 124 | return this; 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/core/Binding.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.core; 2 | 3 | /** 4 | * Represent an operation of {@link io.r2dbc.spi.Statement#bind}. 5 | * 6 | * @author Tadaya Tsuyukubo 7 | * @see Bindings.IndexBinding 8 | * @see Bindings.IdentifierBinding 9 | */ 10 | public interface Binding { 11 | 12 | Object getKey(); 13 | 14 | BindingValue getBindingValue(); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/core/BindingValue.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.core; 2 | 3 | /** 4 | * Represent a value for {@link io.r2dbc.spi.Statement#bind} and {@link io.r2dbc.spi.Statement#bindNull} operations. 5 | * 6 | * @author Tadaya Tsuyukubo 7 | */ 8 | public interface BindingValue { 9 | 10 | Object getValue(); 11 | 12 | class NullBindingValue implements BindingValue { 13 | 14 | private Class type; // type of null 15 | 16 | public NullBindingValue(Class type) { 17 | this.type = type; 18 | } 19 | 20 | @Override 21 | public Object getValue() { 22 | return null; // value is always null 23 | } 24 | 25 | public Class getType() { 26 | return type; 27 | } 28 | } 29 | 30 | class SimpleBindingValue implements BindingValue { 31 | 32 | private Object value; 33 | 34 | public SimpleBindingValue(Object value) { 35 | this.value = value; 36 | } 37 | 38 | @Override 39 | public Object getValue() { 40 | return this.value; 41 | } 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/core/Bindings.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.core; 2 | 3 | import reactor.util.annotation.NonNull; 4 | 5 | import java.util.Objects; 6 | import java.util.SortedSet; 7 | import java.util.TreeSet; 8 | 9 | import static java.util.Comparator.naturalOrder; 10 | 11 | /** 12 | * @author Tadaya Tsuyukubo 13 | */ 14 | public class Bindings { 15 | 16 | private SortedSet indexBindings = new TreeSet<>(); 17 | private SortedSet identifierBindings = new TreeSet<>(); 18 | 19 | public void addIndexBinding(int index, BindingValue value) { 20 | this.indexBindings.add(new IndexBinding(index, value)); 21 | } 22 | 23 | public void addIdentifierBinding(Object identifier, BindingValue value) { 24 | this.identifierBindings.add(new IdentifierBinding(identifier, value)); 25 | } 26 | 27 | public SortedSet getIndexBindings() { 28 | return indexBindings; 29 | } 30 | 31 | public SortedSet getIdentifierBindings() { 32 | return identifierBindings; 33 | } 34 | 35 | public static class IndexBinding implements Binding, Comparable { 36 | private int index; 37 | private BindingValue value; 38 | 39 | public IndexBinding(int index, BindingValue value) { 40 | this.index = index; 41 | this.value = value; 42 | } 43 | 44 | @Override 45 | public int compareTo(@NonNull IndexBinding o) { 46 | return Integer.compare(this.index, o.index); 47 | } 48 | 49 | @Override 50 | public Object getKey() { 51 | return this.index; 52 | } 53 | 54 | @Override 55 | public BindingValue getBindingValue() { 56 | return this.value; 57 | } 58 | } 59 | 60 | public static class IdentifierBinding implements Binding, Comparable { 61 | private Object identifier; 62 | private BindingValue value; 63 | 64 | public IdentifierBinding(Object identifier, BindingValue value) { 65 | this.identifier = identifier; 66 | this.value = value; 67 | } 68 | 69 | @Override 70 | public int compareTo(@NonNull IdentifierBinding o) { 71 | // TODO: implement 72 | return Objects.compare((Comparable) this.identifier, 73 | (Comparable) o.identifier, naturalOrder()); 74 | } 75 | 76 | @Override 77 | public Object getKey() { 78 | return this.identifier; 79 | } 80 | 81 | @Override 82 | public BindingValue getBindingValue() { 83 | return this.value; 84 | } 85 | 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/core/CompositeProxyExecutionListener.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.core; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.Collection; 6 | import java.util.List; 7 | 8 | /** 9 | * Delegate to multiple of {@link ProxyExecutionListener ProxyExecutionListeners}. 10 | * 11 | * @author Tadaya Tsuyukubo 12 | */ 13 | public class CompositeProxyExecutionListener implements ProxyExecutionListener { 14 | private List listeners = new ArrayList<>(); 15 | 16 | public CompositeProxyExecutionListener(ProxyExecutionListener... listeners) { 17 | this.listeners.addAll(Arrays.asList(listeners)); 18 | } 19 | 20 | @Override 21 | public void onMethodExecution(MethodExecutionInfo executionInfo) { 22 | this.listeners.forEach(listener -> listener.onMethodExecution(executionInfo)); 23 | } 24 | 25 | @Override 26 | public void onQueryExecution(QueryExecutionInfo executionInfo) { 27 | this.listeners.forEach(listener -> listener.onQueryExecution(executionInfo)); 28 | } 29 | 30 | @Override 31 | public void beforeMethod(MethodExecutionInfo executionInfo) { 32 | this.listeners.forEach(listener -> listener.beforeMethod(executionInfo)); 33 | } 34 | 35 | @Override 36 | public void afterMethod(MethodExecutionInfo executionInfo) { 37 | this.listeners.forEach(listener -> listener.afterMethod(executionInfo)); 38 | } 39 | 40 | @Override 41 | public void beforeQuery(QueryExecutionInfo execInfo) { 42 | this.listeners.forEach(listener -> listener.beforeQuery(execInfo)); 43 | } 44 | 45 | @Override 46 | public void afterQuery(QueryExecutionInfo execInfo) { 47 | this.listeners.forEach(listener -> listener.afterQuery(execInfo)); 48 | } 49 | 50 | @Override 51 | public void eachQueryResult(QueryExecutionInfo execInfo) { 52 | this.listeners.forEach(listener -> listener.eachQueryResult(execInfo)); 53 | } 54 | 55 | public boolean add(ProxyExecutionListener listener) { 56 | return this.listeners.add(listener); 57 | } 58 | 59 | public boolean addAll(Collection listeners) { 60 | return this.listeners.addAll(listeners); 61 | } 62 | 63 | public List getListeners() { 64 | return this.listeners; 65 | } 66 | 67 | // public void setListeners(List listeners) { 68 | // this.listeners = listeners; 69 | // } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/core/ConnectionHolder.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.core; 2 | 3 | import io.r2dbc.spi.Connection; 4 | import net.ttddyy.dsproxy.r2dbc.proxy.ProxyFactory; 5 | 6 | /** 7 | * Provide methods to retrieve {@link Connection} from proxy object. 8 | * 9 | * {@link Connection}, {@link io.r2dbc.spi.Batch}, and {@link io.r2dbc.spi.Statement} created 10 | * by {@link ProxyFactory} implement this interface. 11 | * 12 | * @author Tadaya Tsuyukubo 13 | * @see Connection 14 | * @see io.r2dbc.spi.Batch 15 | * @see io.r2dbc.spi.Statement 16 | */ 17 | public interface ConnectionHolder { 18 | 19 | Connection getOriginalConnection(); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/core/ConnectionIdManager.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.core; 2 | 3 | import io.r2dbc.spi.Connection; 4 | 5 | /** 6 | * @author Tadaya Tsuyukubo 7 | */ 8 | public interface ConnectionIdManager { 9 | 10 | String getId(Connection connection); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/core/ConnectionInfo.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.core; 2 | 3 | 4 | import io.r2dbc.spi.Connection; 5 | 6 | import java.util.concurrent.atomic.AtomicBoolean; 7 | import java.util.concurrent.atomic.AtomicInteger; 8 | 9 | /** 10 | * {@link Connection} related information. 11 | * 12 | * @author Tadaya Tsuyukubo 13 | */ 14 | public class ConnectionInfo { 15 | 16 | private Connection originalConnection; 17 | private String connectionId; 18 | private AtomicBoolean isClosed = new AtomicBoolean(); 19 | private AtomicInteger transactionCount = new AtomicInteger(); 20 | private AtomicInteger commitCount = new AtomicInteger(); 21 | private AtomicInteger rollbackCount = new AtomicInteger(); 22 | 23 | // TODO: may keep transaction isolation level 24 | 25 | public Connection getOriginalConnection() { 26 | return this.originalConnection; 27 | } 28 | 29 | public void setOriginalConnection(Connection originalConnection) { 30 | this.originalConnection = originalConnection; 31 | } 32 | 33 | public String getConnectionId() { 34 | return this.connectionId; 35 | } 36 | 37 | public void setConnectionId(String connectionId) { 38 | this.connectionId = connectionId; 39 | } 40 | 41 | /** 42 | * Increment transaction count. 43 | */ 44 | public void incrementTransactionCount() { 45 | this.transactionCount.incrementAndGet(); 46 | } 47 | 48 | /** 49 | * Increment commit count. 50 | */ 51 | public void incrementCommitCount() { 52 | this.commitCount.incrementAndGet(); 53 | } 54 | 55 | /** 56 | * Increment rollback count. 57 | */ 58 | public void incrementRollbackCount() { 59 | this.rollbackCount.incrementAndGet(); 60 | } 61 | 62 | 63 | /** 64 | * Returns how many times {@link Connection#beginTransaction()} method is called. 65 | * 66 | * @return num of beginTransaction() method being called 67 | */ 68 | public int getTransactionCount() { 69 | return this.transactionCount.get(); 70 | } 71 | 72 | /** 73 | * Returns how many times {@link Connection#commitTransaction()} method is called. 74 | * 75 | * @return num of commitTransaction method being called 76 | */ 77 | public int getCommitCount() { 78 | return this.commitCount.get(); 79 | } 80 | 81 | /** 82 | * Returns how many times {@link Connection#rollbackTransaction()} method is called. 83 | * 84 | * @return num of rollback methods being called 85 | */ 86 | public int getRollbackCount() { 87 | return this.rollbackCount.get(); 88 | } 89 | 90 | /** 91 | */ 92 | public boolean isClosed() { 93 | return this.isClosed.get(); 94 | } 95 | 96 | /** 97 | */ 98 | public void setClosed(boolean closed) { 99 | this.isClosed.set(closed); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/core/DefaultConnectionIdManager.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.core; 2 | 3 | import io.r2dbc.spi.Connection; 4 | 5 | import java.util.concurrent.atomic.AtomicLong; 6 | 7 | /** 8 | * Copy from datasource-proxy 9 | * 10 | * TODO: manage open connection ids 11 | * 12 | * @author Tadaya Tsuyukubo 13 | */ 14 | public class DefaultConnectionIdManager implements ConnectionIdManager { 15 | 16 | private AtomicLong idCounter = new AtomicLong(0); 17 | 18 | @Override 19 | public String getId(Connection connection) { 20 | String id = String.valueOf(this.idCounter.incrementAndGet()); 21 | return id; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/core/ExecutionType.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.core; 2 | 3 | /** 4 | * 5 | * @author Tadaya Tsuyukubo 6 | */ 7 | public enum ExecutionType { 8 | BATCH, STATEMENT 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/core/MethodExecutionInfo.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.core; 2 | 3 | import java.lang.reflect.Method; 4 | import java.time.Duration; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | /** 9 | * Hold method execution related information. 10 | * 11 | * @author Tadaya Tsuyukubo 12 | */ 13 | public class MethodExecutionInfo { 14 | 15 | private Object target; 16 | private Method method; 17 | private Object[] methodArgs; 18 | private Object result; 19 | private Throwable thrown; 20 | private ConnectionInfo connectionInfo; 21 | 22 | private Duration executeDuration = Duration.ZERO; 23 | private String threadName; 24 | private long threadId; 25 | private ProxyEventType proxyEventType; 26 | private Map customValues = new HashMap<>(); 27 | 28 | /** 29 | * Store key/value pair. 30 | * 31 | * Mainly used for passing values between before and after listener callback. 32 | * 33 | * @param key key 34 | * @param value value 35 | */ 36 | public void addCustomValue(String key, Object value) { 37 | this.customValues.put(key, value); 38 | } 39 | 40 | public T getCustomValue(String key, Class type) { 41 | return type.cast(this.customValues.get(key)); 42 | } 43 | 44 | public Object getTarget() { 45 | return target; 46 | } 47 | 48 | public void setTarget(Object target) { 49 | this.target = target; 50 | } 51 | 52 | public Method getMethod() { 53 | return method; 54 | } 55 | 56 | public void setMethod(Method method) { 57 | this.method = method; 58 | } 59 | 60 | public Object[] getMethodArgs() { 61 | return methodArgs; 62 | } 63 | 64 | public void setMethodArgs(Object[] methodArgs) { 65 | this.methodArgs = methodArgs; 66 | } 67 | 68 | public Object getResult() { 69 | return result; 70 | } 71 | 72 | public void setResult(Object result) { 73 | this.result = result; 74 | } 75 | 76 | public Throwable getThrown() { 77 | return thrown; 78 | } 79 | 80 | public void setThrown(Throwable thrown) { 81 | this.thrown = thrown; 82 | } 83 | 84 | public ConnectionInfo getConnectionInfo() { 85 | return this.connectionInfo; 86 | } 87 | 88 | public void setConnectionInfo(ConnectionInfo connectionInfo) { 89 | this.connectionInfo = connectionInfo; 90 | } 91 | 92 | public Duration getExecuteDuration() { 93 | return executeDuration; 94 | } 95 | 96 | public void setExecuteDuration(Duration executeDuration) { 97 | this.executeDuration = executeDuration; 98 | } 99 | 100 | public String getThreadName() { 101 | return threadName; 102 | } 103 | 104 | public void setThreadName(String threadName) { 105 | this.threadName = threadName; 106 | } 107 | 108 | public long getThreadId() { 109 | return threadId; 110 | } 111 | 112 | public void setThreadId(long threadId) { 113 | this.threadId = threadId; 114 | } 115 | 116 | public ProxyEventType getProxyEventType() { 117 | return proxyEventType; 118 | } 119 | 120 | public void setProxyEventType(ProxyEventType proxyEventType) { 121 | this.proxyEventType = proxyEventType; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/core/ProxyEventType.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.core; 2 | 3 | /** 4 | * @author Tadaya Tsuyukubo 5 | */ 6 | public enum ProxyEventType { 7 | BEFORE_METHOD, AFTER_METHOD, BEFORE_QUERY, AFTER_QUERY, EACH_QUERY_RESULT 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/core/ProxyExecutionListener.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.core; 2 | 3 | import io.r2dbc.spi.Batch; 4 | import io.r2dbc.spi.Statement; 5 | 6 | import java.util.function.BiFunction; 7 | 8 | /** 9 | * Listener interface that is called by proxy on {@link io.r2dbc.spi.Connection}, 10 | * {@link io.r2dbc.spi.Statement}, or {@link io.r2dbc.spi.Batch}. 11 | * 12 | * @author Tadaya Tsuyukubo 13 | */ 14 | public interface ProxyExecutionListener { 15 | 16 | /** 17 | * Called before and after every invocation of methods. 18 | * 19 | * Default implementation of this method dispatches to {@link #beforeMethod(MethodExecutionInfo)} and 20 | * {@link #afterMethod(MethodExecutionInfo)}. Thus overriding this method will stop calling them 21 | * unless subclass calls {@code super.onMethodExecution(executionInfo);}. 22 | * 23 | * @param executionInfo query execution context 24 | */ 25 | default void onMethodExecution(MethodExecutionInfo executionInfo) { 26 | ProxyEventType eventType = executionInfo.getProxyEventType(); 27 | if (eventType == ProxyEventType.BEFORE_METHOD) { 28 | beforeMethod(executionInfo); 29 | } else if (eventType == ProxyEventType.AFTER_METHOD) { 30 | afterMethod(executionInfo); 31 | } 32 | } 33 | 34 | /** 35 | * Called before and after execution of query. 36 | * 37 | * Default implementation of this method dispatches to {@link #beforeQuery(QueryExecutionInfo)} and 38 | * {@link #afterQuery(QueryExecutionInfo)}. Thus overriding this method will stop calling them 39 | * unless subclass calls {@code super.onQueryExecution(executionInfo);}. 40 | * 41 | * @param executionInfo query execution context 42 | */ 43 | default void onQueryExecution(QueryExecutionInfo executionInfo) { 44 | ProxyEventType eventType = executionInfo.getProxyEventType(); 45 | if (eventType == ProxyEventType.BEFORE_QUERY) { 46 | beforeQuery(executionInfo); 47 | } else if (eventType == ProxyEventType.AFTER_QUERY) { 48 | afterQuery(executionInfo); 49 | } 50 | } 51 | 52 | /** 53 | * Called before every invocation of methods. 54 | * 55 | * @param executionInfo method execution context 56 | */ 57 | default void beforeMethod(MethodExecutionInfo executionInfo) { 58 | } 59 | 60 | /** 61 | * Called after every invocation of methods. 62 | * 63 | * @param executionInfo method execution context 64 | */ 65 | default void afterMethod(MethodExecutionInfo executionInfo) { 66 | } 67 | 68 | /** 69 | * Called before execution of query. 70 | * 71 | * Query execution is {@link Batch#execute()} or {@link Statement#execute()}. 72 | * 73 | * Note: this callback is called when the publisher, result of the {@code execute()}, is being 74 | * subscribed. Not at the time of {@code execute()} is called, 75 | * 76 | * @param execInfo query execution context 77 | */ 78 | default void beforeQuery(QueryExecutionInfo execInfo) { 79 | } 80 | 81 | /** 82 | * Called after execution of query. 83 | * 84 | * Query execution is {@link Batch#execute()} or {@link Statement#execute()}. 85 | * 86 | * Note: this callback is called when the publisher, result of the {@code execute()}, is being 87 | * subscribed. Not at the time of {@code execute()} is called, 88 | * 89 | * @param execInfo query execution context 90 | */ 91 | default void afterQuery(QueryExecutionInfo execInfo) { 92 | } 93 | 94 | /** 95 | * Called on processing each query {@link io.r2dbc.spi.Result}. 96 | * 97 | * While processing query results with {@link io.r2dbc.spi.Result#map(BiFunction)}, this callback 98 | * is called per result. 99 | * {@link QueryExecutionInfo#getCurrentResult()} contains the mapped result. 100 | * 101 | * @param execInfo query execution context 102 | */ 103 | default void eachQueryResult(QueryExecutionInfo execInfo) { 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/core/QueryExecutionInfo.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.core; 2 | 3 | import io.r2dbc.spi.Result; 4 | 5 | import java.lang.reflect.Method; 6 | import java.time.Duration; 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | /** 13 | * Hold query execution related information. 14 | * 15 | * @author Tadaya Tsuyukubo 16 | */ 17 | public class QueryExecutionInfo { 18 | 19 | private ConnectionInfo connectionInfo; 20 | private Method method; 21 | private Object[] methodArgs; 22 | private Throwable throwable; 23 | private boolean isSuccess; 24 | private int batchSize; // num of Batch#add 25 | 26 | private ExecutionType type; 27 | private int bindingsSize; // num of Statement#add 28 | private Duration executeDuration = Duration.ZERO; 29 | private String threadName = ""; 30 | private long threadId; 31 | private ProxyEventType proxyEventType; 32 | private int currentResultCount; 33 | private Object currentMappedResult; 34 | private List queries = new ArrayList<>(); 35 | private Map customValues = new HashMap<>(); 36 | 37 | /** 38 | * Store key/value pair. 39 | * 40 | * Mainly used for passing values between before and after listener callback. 41 | * 42 | * @param key key 43 | * @param value value 44 | */ 45 | public void addCustomValue(String key, Object value) { 46 | this.customValues.put(key, value); 47 | } 48 | 49 | public T getCustomValue(String key, Class type) { 50 | return type.cast(this.customValues.get(key)); 51 | } 52 | 53 | public Method getMethod() { 54 | return method; 55 | } 56 | 57 | public void setMethod(Method method) { 58 | this.method = method; 59 | } 60 | 61 | public Object[] getMethodArgs() { 62 | return methodArgs; 63 | } 64 | 65 | public void setMethodArgs(Object[] methodArgs) { 66 | this.methodArgs = methodArgs; 67 | } 68 | 69 | public ConnectionInfo getConnectionInfo() { 70 | return this.connectionInfo; 71 | } 72 | 73 | public void setConnectionInfo(ConnectionInfo connectionInfo) { 74 | this.connectionInfo = connectionInfo; 75 | } 76 | 77 | public Throwable getThrowable() { 78 | return throwable; 79 | } 80 | 81 | /** 82 | * Contains an exception thrown while query was executed. 83 | * Contains value only when an exception has thrown, otherwise {@code null}. 84 | * 85 | * @param throwable an error thrown while executing a query 86 | */ 87 | public void setThrowable(Throwable throwable) { 88 | this.throwable = throwable; 89 | } 90 | 91 | /** 92 | * Indicate whether the query execution was successful or not. 93 | * Contains valid value only after the query execution. 94 | * 95 | * @return true when query has successfully executed 96 | */ 97 | public boolean isSuccess() { 98 | return isSuccess; 99 | } 100 | 101 | public void setSuccess(boolean isSuccess) { 102 | this.isSuccess = isSuccess; 103 | } 104 | 105 | public int getBatchSize() { 106 | return batchSize; 107 | } 108 | 109 | public void setBatchSize(int batchSize) { 110 | this.batchSize = batchSize; 111 | } 112 | 113 | /** 114 | * Returns list of {@link QueryInfo}. 115 | * 116 | * @return list of queries. This will NOT return null. 117 | */ 118 | public List getQueries() { 119 | return this.queries; 120 | } 121 | 122 | public void setQueries(List queries) { 123 | this.queries = queries; 124 | } 125 | 126 | public ExecutionType getType() { 127 | return type; 128 | } 129 | 130 | public void setType(ExecutionType type) { 131 | this.type = type; 132 | } 133 | 134 | public int getBindingsSize() { 135 | return bindingsSize; 136 | } 137 | 138 | public void setBindingsSize(int bindingsSize) { 139 | this.bindingsSize = bindingsSize; 140 | } 141 | 142 | /** 143 | * Time that took queries to execute. 144 | * 145 | * @return query execution duration 146 | */ 147 | public Duration getExecuteDuration() { 148 | return executeDuration; 149 | } 150 | 151 | public void setExecuteDuration(Duration executeDuration) { 152 | this.executeDuration = executeDuration; 153 | } 154 | 155 | public String getThreadName() { 156 | return threadName; 157 | } 158 | 159 | public void setThreadName(String threadName) { 160 | this.threadName = threadName; 161 | } 162 | 163 | public long getThreadId() { 164 | return threadId; 165 | } 166 | 167 | public void setThreadId(long threadId) { 168 | this.threadId = threadId; 169 | } 170 | 171 | public ProxyEventType getProxyEventType() { 172 | return proxyEventType; 173 | } 174 | 175 | public void setProxyEventType(ProxyEventType proxyEventType) { 176 | this.proxyEventType = proxyEventType; 177 | } 178 | 179 | /** 180 | * Represent Nth {@link io.r2dbc.spi.Result}. 181 | * 182 | * On each query result callback({@link ProxyExecutionListener#eachQueryResult(QueryExecutionInfo)}), 183 | * this value indicates Nth {@link Result} starting from 1. 184 | * (1st query result, 2nd query result, 3rd, 4th,...). 185 | * 186 | * This returns 0 for before query execution({@link ProxyExecutionListener#beforeQuery(QueryExecutionInfo)}). 187 | * For after query execution({@link ProxyExecutionListener#afterQuery(QueryExecutionInfo)}), this returns 188 | * total number of {@link io.r2dbc.spi.Result} returned by this query execution. 189 | * 190 | * @return Nth number of query result 191 | */ 192 | public int getCurrentResultCount() { 193 | return currentResultCount; 194 | } 195 | 196 | public void setCurrentResultCount(int currentResultCount) { 197 | this.currentResultCount = currentResultCount; 198 | } 199 | 200 | /** 201 | * Mapped query result available for each-query-result-callback({@link ProxyExecutionListener#eachQueryResult(QueryExecutionInfo)}). 202 | * 203 | * For before and after query execution({@link ProxyExecutionListener#beforeQuery(QueryExecutionInfo)} 204 | * and {@link ProxyExecutionListener#afterQuery(QueryExecutionInfo)), this returns {@code null}. 205 | * 206 | * @return 207 | */ 208 | public Object getCurrentMappedResult() { 209 | return currentMappedResult; 210 | } 211 | 212 | public void setCurrentMappedResult(Object currentResult) { 213 | this.currentMappedResult = currentResult; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/core/QueryInfo.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.core; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * 8 | * @author Tadaya Tsuyukubo 9 | */ 10 | public class QueryInfo { 11 | 12 | private String query; 13 | 14 | private List bindingsList = new ArrayList<>(); 15 | 16 | public QueryInfo() { 17 | } 18 | 19 | public QueryInfo(String query) { 20 | this.query = query; 21 | } 22 | 23 | public String getQuery() { 24 | return query; 25 | } 26 | 27 | public void setQuery(String query) { 28 | this.query = query; 29 | } 30 | 31 | // TODO: improve 32 | public List getBindingsList() { 33 | return bindingsList; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/proxy/CallbackSupport.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Result; 4 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 5 | import net.ttddyy.dsproxy.r2dbc.core.MethodExecutionInfo; 6 | import net.ttddyy.dsproxy.r2dbc.core.ProxyEventType; 7 | import net.ttddyy.dsproxy.r2dbc.core.ProxyExecutionListener; 8 | import net.ttddyy.dsproxy.r2dbc.core.QueryExecutionInfo; 9 | import org.reactivestreams.Publisher; 10 | import reactor.core.publisher.Flux; 11 | import reactor.core.publisher.Mono; 12 | 13 | import java.lang.reflect.InvocationTargetException; 14 | import java.lang.reflect.Method; 15 | import java.time.Clock; 16 | import java.time.Duration; 17 | import java.time.Instant; 18 | import java.util.Arrays; 19 | import java.util.Set; 20 | import java.util.concurrent.atomic.AtomicReference; 21 | import java.util.function.BiFunction; 22 | import java.util.function.Consumer; 23 | 24 | import static java.util.stream.Collectors.toSet; 25 | 26 | /** 27 | * @author Tadaya Tsuyukubo 28 | */ 29 | public abstract class CallbackSupport { 30 | 31 | private static final Set PASS_THROUGH_METHODS; 32 | 33 | static { 34 | try { 35 | Method objectToStringMethod = Object.class.getMethod("toString"); 36 | PASS_THROUGH_METHODS = Arrays.stream(Object.class.getMethods()) 37 | .filter(method -> !objectToStringMethod.equals(method)) 38 | .collect(toSet()); 39 | 40 | } catch (NoSuchMethodException e) { 41 | throw new RuntimeException(e); 42 | } 43 | } 44 | 45 | protected Clock clock = Clock.systemUTC(); 46 | 47 | protected ProxyConfig proxyConfig; 48 | 49 | 50 | public CallbackSupport(ProxyConfig proxyConfig) { 51 | this.proxyConfig = proxyConfig; 52 | } 53 | 54 | /** 55 | * Augment method invocation and call method listener. 56 | * 57 | * @param method method to invoke on target 58 | * @param target an object being invoked 59 | * @param args arguments for the method 60 | * @param listener listener that before/aftre method callbacks will be called 61 | * @param connectionInfo current connection information 62 | * @param onMap a callback that will be chained on "map()" right after the result of the method invocation 63 | * @param onComplete a callback that will be chained as the first doOnComplete on the result of the method invocation 64 | * @return 65 | * @throws Throwable 66 | */ 67 | protected Object proceedExecution(Method method, Object target, Object[] args, 68 | ProxyExecutionListener listener, ConnectionInfo connectionInfo, 69 | BiFunction onMap, 70 | Consumer onComplete) throws Throwable { 71 | 72 | if (PASS_THROUGH_METHODS.contains(method)) { 73 | try { 74 | return method.invoke(target, args); 75 | } catch (InvocationTargetException ex) { 76 | throw ex.getTargetException(); 77 | } 78 | } 79 | 80 | // special handling for toString() 81 | if ("toString".equals(method.getName())) { 82 | StringBuilder sb = new StringBuilder(); 83 | sb.append(target.getClass().getSimpleName()); // ConnectionFactory, Connection, Batch, or Statement 84 | sb.append("-proxy ["); 85 | sb.append(target.toString()); 86 | sb.append("]"); 87 | return sb.toString(); // differentiate toString message. 88 | } 89 | 90 | 91 | AtomicReference startTimeHolder = new AtomicReference<>(); 92 | 93 | MethodExecutionInfo executionInfo = new MethodExecutionInfo(); 94 | executionInfo.setMethod(method); 95 | executionInfo.setMethodArgs(args); 96 | executionInfo.setTarget(target); 97 | executionInfo.setConnectionInfo(connectionInfo); 98 | 99 | Class returnType = method.getReturnType(); 100 | 101 | if (Publisher.class.isAssignableFrom(returnType)) { 102 | 103 | Publisher result; 104 | try { 105 | result = (Publisher) method.invoke(target, args); 106 | } catch (InvocationTargetException ex) { 107 | throw ex.getTargetException(); 108 | } 109 | 110 | return Flux.empty() 111 | .doOnSubscribe(s -> { 112 | 113 | Instant startTime = this.clock.instant(); 114 | startTimeHolder.set(startTime); 115 | 116 | String threadName = Thread.currentThread().getName(); 117 | long threadId = Thread.currentThread().getId(); 118 | executionInfo.setThreadName(threadName); 119 | executionInfo.setThreadId(threadId); 120 | 121 | executionInfo.setProxyEventType(ProxyEventType.BEFORE_METHOD); 122 | 123 | listener.onMethodExecution(executionInfo); 124 | }) 125 | .concatWith(result) 126 | .map(resultObj -> { 127 | 128 | // set produced object as result 129 | executionInfo.setResult(resultObj); 130 | 131 | // apply a function to flux-chain right after the original publisher operations 132 | if (onMap != null) { 133 | return onMap.apply(resultObj, executionInfo); 134 | } 135 | return resultObj; 136 | }) 137 | .doOnComplete(() -> { 138 | // apply a consumer to flux-chain right after the original publisher operations 139 | // this is the first chained doOnComplete on the result publisher 140 | if (onComplete != null) { 141 | onComplete.accept(executionInfo); 142 | } 143 | }) 144 | .doOnError(throwable -> { 145 | executionInfo.setThrown(throwable); 146 | }) 147 | .doFinally(signalType -> { 148 | 149 | Instant startTime = startTimeHolder.get(); 150 | Instant currentTime = this.clock.instant(); 151 | 152 | Duration executionDuration = Duration.between(startTime, currentTime); 153 | executionInfo.setExecuteDuration(executionDuration); 154 | 155 | String threadName = Thread.currentThread().getName(); 156 | long threadId = Thread.currentThread().getId(); 157 | executionInfo.setThreadName(threadName); 158 | executionInfo.setThreadId(threadId); 159 | 160 | executionInfo.setProxyEventType(ProxyEventType.AFTER_METHOD); 161 | 162 | listener.onMethodExecution(executionInfo); 163 | }); 164 | 165 | 166 | } else { 167 | // for method that generates non-publisher, execution happens when it is invoked. 168 | 169 | String threadName = Thread.currentThread().getName(); 170 | long threadId = Thread.currentThread().getId(); 171 | executionInfo.setThreadName(threadName); 172 | executionInfo.setThreadId(threadId); 173 | 174 | // invoke before method 175 | executionInfo.setProxyEventType(ProxyEventType.BEFORE_METHOD); 176 | listener.onMethodExecution(executionInfo); 177 | 178 | Instant startTime = this.clock.instant(); 179 | 180 | Object result = null; 181 | Throwable thrown = null; 182 | try { 183 | result = method.invoke(target, args); 184 | } catch (InvocationTargetException ex) { 185 | thrown = ex.getTargetException(); 186 | throw thrown; 187 | } finally { 188 | executionInfo.setResult(result); 189 | executionInfo.setThrown(thrown); 190 | 191 | Instant currentTime = this.clock.instant(); 192 | Duration executionDuration = Duration.between(startTime, currentTime); 193 | executionInfo.setExecuteDuration(executionDuration); 194 | 195 | executionInfo.setProxyEventType(ProxyEventType.AFTER_METHOD); 196 | listener.onMethodExecution(executionInfo); 197 | } 198 | return result; 199 | 200 | } 201 | 202 | } 203 | 204 | /** 205 | * Augment query execution result to hook up listener lifecycle. 206 | */ 207 | protected Flux interceptQueryExecution(Publisher flux, QueryExecutionInfo executionInfo) { 208 | 209 | ProxyExecutionListener listener = this.proxyConfig.getListeners(); 210 | 211 | AtomicReference startTimeHolder = new AtomicReference<>(); 212 | 213 | Flux queryExecutionFlux = Flux.empty() 214 | .ofType(Result.class) 215 | .doOnSubscribe(s -> { 216 | 217 | Instant startTime = this.clock.instant(); 218 | startTimeHolder.set(startTime); 219 | 220 | String threadName = Thread.currentThread().getName(); 221 | long threadId = Thread.currentThread().getId(); 222 | executionInfo.setThreadName(threadName); 223 | executionInfo.setThreadId(threadId); 224 | 225 | executionInfo.setCurrentMappedResult(null); 226 | 227 | executionInfo.setProxyEventType(ProxyEventType.BEFORE_QUERY); 228 | 229 | listener.onQueryExecution(executionInfo); 230 | }) 231 | .concatWith(flux) 232 | .doOnComplete(() -> { 233 | executionInfo.setSuccess(true); 234 | }) 235 | .doOnError(throwable -> { 236 | executionInfo.setThrowable(throwable); 237 | executionInfo.setSuccess(false); 238 | }) 239 | .doFinally(signalType -> { 240 | 241 | Instant startTime = startTimeHolder.get(); 242 | Instant currentTime = this.clock.instant(); 243 | 244 | Duration executionDuration = Duration.between(startTime, currentTime); 245 | executionInfo.setExecuteDuration(executionDuration); 246 | 247 | String threadName = Thread.currentThread().getName(); 248 | long threadId = Thread.currentThread().getId(); 249 | executionInfo.setThreadName(threadName); 250 | executionInfo.setThreadId(threadId); 251 | 252 | executionInfo.setCurrentMappedResult(null); 253 | 254 | executionInfo.setProxyEventType(ProxyEventType.AFTER_QUERY); 255 | 256 | listener.onQueryExecution(executionInfo); 257 | }); 258 | 259 | ProxyFactory proxyFactory = this.proxyConfig.getProxyFactory(); 260 | 261 | // return a publisher that returns proxy Result 262 | return Flux.from(queryExecutionFlux) 263 | .flatMap(queryResult -> Mono.just(proxyFactory.createResult(queryResult, executionInfo))); 264 | 265 | } 266 | 267 | public void setClock(Clock clock) { 268 | this.clock = clock; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/proxy/JdkProxyFactory.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Batch; 4 | import io.r2dbc.spi.Connection; 5 | import io.r2dbc.spi.ConnectionFactory; 6 | import io.r2dbc.spi.Result; 7 | import io.r2dbc.spi.Statement; 8 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionHolder; 9 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 10 | import net.ttddyy.dsproxy.r2dbc.core.QueryExecutionInfo; 11 | 12 | import java.lang.reflect.InvocationHandler; 13 | import java.lang.reflect.Method; 14 | import java.lang.reflect.Proxy; 15 | 16 | /** 17 | * ProxyFactory using JDK dynamic proxy. 18 | * 19 | * @author Tadaya Tsuyukubo 20 | */ 21 | public class JdkProxyFactory implements ProxyFactory { 22 | 23 | private ProxyConfig proxyConfig; 24 | 25 | @Override 26 | public void setProxyConfig(ProxyConfig proxyConfig) { 27 | this.proxyConfig = proxyConfig; 28 | } 29 | 30 | @Override 31 | public ConnectionFactory createConnectionFactory(ConnectionFactory connectionFactory) { 32 | return (ConnectionFactory) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), 33 | new Class[]{ConnectionFactory.class, ProxyObject.class}, 34 | new ConnectionFactoryInvocationHandler(connectionFactory, this.proxyConfig)); 35 | } 36 | 37 | @Override 38 | public Connection createConnection(Connection connection, ConnectionInfo connectionInfo) { 39 | return (Connection) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), 40 | new Class[]{Connection.class, ProxyObject.class, ConnectionHolder.class}, 41 | new ConnectionInvocationHandler(connection, connectionInfo, this.proxyConfig)); 42 | } 43 | 44 | @Override 45 | public Batch createBatch(Batch batch, ConnectionInfo connectionInfo) { 46 | return (Batch) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), 47 | new Class[]{Batch.class, ProxyObject.class, ConnectionHolder.class}, 48 | new BatchInvocationHandler(batch, connectionInfo, this.proxyConfig)); 49 | } 50 | 51 | @Override 52 | public Statement createStatement(Statement statement, String query, ConnectionInfo connectionInfo) { 53 | return (Statement) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), 54 | new Class[]{Statement.class, ProxyObject.class, ConnectionHolder.class}, 55 | new StatementInvocationHandler(statement, query, connectionInfo, this.proxyConfig)); 56 | } 57 | 58 | @Override 59 | public Result createResult(Result result, QueryExecutionInfo queryExecutionInfo) { 60 | return (Result) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), 61 | new Class[]{Result.class, ProxyObject.class, ConnectionHolder.class}, 62 | new ResultInvocationHandler(result, queryExecutionInfo, this.proxyConfig)); 63 | } 64 | 65 | public static class ConnectionFactoryInvocationHandler implements InvocationHandler { 66 | 67 | private ReactiveConnectionFactoryCallback delegate; 68 | 69 | public ConnectionFactoryInvocationHandler(ConnectionFactory connectionFactory, ProxyConfig proxyConfig) { 70 | this.delegate = new ReactiveConnectionFactoryCallback(connectionFactory, proxyConfig); 71 | } 72 | 73 | @Override 74 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 75 | return delegate.invoke(proxy, method, args); 76 | } 77 | } 78 | 79 | public static class ConnectionInvocationHandler implements InvocationHandler { 80 | 81 | private ReactiveConnectionCallback delegate; 82 | 83 | public ConnectionInvocationHandler(Connection connection, ConnectionInfo connectionInfo, ProxyConfig proxyConfig) { 84 | this.delegate = new ReactiveConnectionCallback(connection, connectionInfo, proxyConfig); 85 | } 86 | 87 | @Override 88 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 89 | return delegate.invoke(proxy, method, args); 90 | } 91 | } 92 | 93 | public static class BatchInvocationHandler implements InvocationHandler { 94 | 95 | private ReactiveBatchCallback delegate; 96 | 97 | public BatchInvocationHandler(Batch batch, ConnectionInfo connectionInfo, ProxyConfig proxyConfig) { 98 | this.delegate = new ReactiveBatchCallback(batch, connectionInfo, proxyConfig); 99 | } 100 | 101 | @Override 102 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 103 | return delegate.invoke(proxy, method, args); 104 | } 105 | } 106 | 107 | public static class StatementInvocationHandler implements InvocationHandler { 108 | 109 | private ReactiveStatementCallback delegate; 110 | 111 | public StatementInvocationHandler(Statement statement, String query, ConnectionInfo connectionInfo, ProxyConfig proxyConfig) { 112 | this.delegate = new ReactiveStatementCallback(statement, query, connectionInfo, proxyConfig); 113 | } 114 | 115 | @Override 116 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 117 | return delegate.invoke(proxy, method, args); 118 | } 119 | } 120 | 121 | public static class ResultInvocationHandler implements InvocationHandler { 122 | 123 | private ReactiveResultCallback delegate; 124 | 125 | public ResultInvocationHandler(Result result, QueryExecutionInfo queryExecutionInfo, ProxyConfig proxyConfig) { 126 | this.delegate = new ReactiveResultCallback(result, queryExecutionInfo, proxyConfig); 127 | } 128 | 129 | @Override 130 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 131 | return delegate.invoke(proxy, method, args); 132 | } 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/proxy/ProxyConfig.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import net.ttddyy.dsproxy.r2dbc.core.CompositeProxyExecutionListener; 4 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionIdManager; 5 | import net.ttddyy.dsproxy.r2dbc.core.DefaultConnectionIdManager; 6 | import net.ttddyy.dsproxy.r2dbc.core.ProxyExecutionListener; 7 | 8 | /** 9 | * 10 | * @author Tadaya Tsuyukubo 11 | */ 12 | public class ProxyConfig { 13 | 14 | private CompositeProxyExecutionListener listeners = new CompositeProxyExecutionListener(); 15 | 16 | private ConnectionIdManager connectionIdManager = new DefaultConnectionIdManager(); 17 | 18 | private ProxyFactory proxyFactory = new JdkProxyFactory(); 19 | 20 | { 21 | this.proxyFactory.setProxyConfig(this); 22 | } 23 | 24 | // public ProxyConfig(ProxyFactory proxyFactory) { 25 | // this.proxyFactory = proxyFactory; 26 | // } 27 | 28 | public ProxyFactory getProxyFactory() { 29 | return proxyFactory; 30 | } 31 | 32 | public void setProxyFactory(ProxyFactory proxyFactory) { 33 | this.proxyFactory = proxyFactory; 34 | this.proxyFactory.setProxyConfig(this); 35 | } 36 | 37 | public CompositeProxyExecutionListener getListeners() { 38 | return this.listeners; 39 | } 40 | 41 | public void addListener(ProxyExecutionListener listener) { 42 | this.listeners.add(listener); 43 | } 44 | 45 | public ConnectionIdManager getConnectionIdManager() { 46 | return connectionIdManager; 47 | } 48 | 49 | public void setConnectionIdManager(ConnectionIdManager connectionIdManager) { 50 | this.connectionIdManager = connectionIdManager; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/proxy/ProxyFactory.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Batch; 4 | import io.r2dbc.spi.Connection; 5 | import io.r2dbc.spi.ConnectionFactory; 6 | import io.r2dbc.spi.Result; 7 | import io.r2dbc.spi.Statement; 8 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 9 | import net.ttddyy.dsproxy.r2dbc.core.QueryExecutionInfo; 10 | 11 | /** 12 | * @author Tadaya Tsuyukubo 13 | */ 14 | public interface ProxyFactory { 15 | 16 | void setProxyConfig(ProxyConfig proxyConfig); 17 | 18 | ConnectionFactory createConnectionFactory(ConnectionFactory connectionFactory); 19 | 20 | Connection createConnection(Connection connection, ConnectionInfo connectionInfo); 21 | 22 | Batch createBatch(Batch batch, ConnectionInfo connectionInfo); 23 | 24 | Statement createStatement(Statement statement, String query, ConnectionInfo connectionInfo); 25 | 26 | Result createResult(Result result, QueryExecutionInfo executionInfo); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/proxy/ProxyObject.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import net.ttddyy.dsproxy.r2dbc.proxy.ProxyFactory; 4 | import net.ttddyy.dsproxy.r2dbc.proxy.ReactiveBatchCallback; 5 | import net.ttddyy.dsproxy.r2dbc.proxy.ReactiveConnectionCallback; 6 | import net.ttddyy.dsproxy.r2dbc.proxy.ReactiveConnectionFactoryCallback; 7 | import net.ttddyy.dsproxy.r2dbc.proxy.ReactiveStatementCallback; 8 | 9 | /** 10 | * Provide a method to unwrap the original object from proxy object. 11 | * 12 | *

Proxy object created by {@link ProxyFactory} implements this interface. 13 | * 14 | * @author Tadaya Tsuyukubo 15 | * @see ProxyFactory 16 | * @see ReactiveConnectionFactoryCallback 17 | * @see ReactiveConnectionCallback 18 | * @see ReactiveBatchCallback 19 | * @see ReactiveStatementCallback 20 | */ 21 | public interface ProxyObject { 22 | 23 | /** 24 | * Method to return the source object (ConnectionFactory, Connection, Batch, Statement). 25 | * 26 | * @return source object 27 | */ 28 | Object getTarget(); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/proxy/ProxyUtils.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Batch; 4 | import io.r2dbc.spi.Connection; 5 | import io.r2dbc.spi.Result; 6 | import io.r2dbc.spi.Statement; 7 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionHolder; 8 | 9 | /** 10 | * @author Tadaya Tsuyukubo 11 | */ 12 | public class ProxyUtils { 13 | 14 | private ProxyUtils() { 15 | } 16 | 17 | public static Connection getOriginalConnection(Connection connection) { 18 | if (connection instanceof ProxyObject) { 19 | return (Connection) ((ProxyObject) connection).getTarget(); 20 | } 21 | return connection; 22 | } 23 | 24 | public static Connection getOriginalConnection(Batch batch) { 25 | if (batch instanceof ConnectionHolder) { 26 | return ((ConnectionHolder) batch).getOriginalConnection(); 27 | } 28 | return null; 29 | } 30 | 31 | public static Connection getOriginalConnection(Statement statement) { 32 | if (statement instanceof ConnectionHolder) { 33 | return ((ConnectionHolder) statement).getOriginalConnection(); 34 | } 35 | return null; 36 | } 37 | 38 | public static Connection getOriginalConnection(Result result) { 39 | if (result instanceof ConnectionHolder) { 40 | return ((ConnectionHolder) result).getOriginalConnection(); 41 | } 42 | return null; 43 | } 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/proxy/ReactiveBatchCallback.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Batch; 4 | import io.r2dbc.spi.Result; 5 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 6 | import net.ttddyy.dsproxy.r2dbc.core.ExecutionType; 7 | import net.ttddyy.dsproxy.r2dbc.core.QueryExecutionInfo; 8 | import net.ttddyy.dsproxy.r2dbc.core.QueryInfo; 9 | import org.reactivestreams.Publisher; 10 | 11 | import java.lang.reflect.Method; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | import static java.util.stream.Collectors.toList; 16 | 17 | /** 18 | * Proxy callback for {@link Batch}. 19 | * 20 | * @author Tadaya Tsuyukubo 21 | */ 22 | public class ReactiveBatchCallback extends CallbackSupport { 23 | 24 | private Batch batch; 25 | 26 | private ConnectionInfo connectionInfo; 27 | private List queries = new ArrayList<>(); 28 | 29 | public ReactiveBatchCallback(Batch batch, ConnectionInfo connectionInfo, ProxyConfig proxyConfig) { 30 | super(proxyConfig); 31 | this.batch = batch; 32 | this.connectionInfo = connectionInfo; 33 | } 34 | 35 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 36 | 37 | String methodName = method.getName(); 38 | 39 | if ("getTarget".equals(methodName)) { 40 | return this.batch; 41 | } else if ("getOriginalConnection".equals(methodName)) { 42 | return this.connectionInfo.getOriginalConnection(); 43 | } 44 | 45 | Object result = proceedExecution(method, this.batch, args, this.proxyConfig.getListeners(), this.connectionInfo, null, null); 46 | 47 | if ("add".equals(methodName)) { 48 | this.queries.add((String) args[0]); 49 | } else if ("execute".equals(methodName)) { 50 | 51 | List queryInfoList = this.queries.stream() 52 | .map(QueryInfo::new) 53 | .collect(toList()); 54 | 55 | QueryExecutionInfo execInfo = new QueryExecutionInfo(); 56 | execInfo.setType(ExecutionType.BATCH); 57 | execInfo.setQueries(queryInfoList); 58 | execInfo.setBatchSize(this.queries.size()); 59 | execInfo.setMethod(method); 60 | execInfo.setMethodArgs(args); 61 | execInfo.setConnectionInfo(this.connectionInfo); 62 | 63 | // API defines "execute()" returns a publisher 64 | Publisher publisher = (Publisher) result; 65 | 66 | return interceptQueryExecution(publisher, execInfo); 67 | } 68 | 69 | return result; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/proxy/ReactiveConnectionCallback.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Batch; 4 | import io.r2dbc.spi.Connection; 5 | import io.r2dbc.spi.Statement; 6 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 7 | import net.ttddyy.dsproxy.r2dbc.core.MethodExecutionInfo; 8 | 9 | import java.lang.reflect.Method; 10 | import java.util.function.Consumer; 11 | 12 | /** 13 | * Proxy callback for {@link Connection}. 14 | * 15 | * @author Tadaya Tsuyukubo 16 | */ 17 | public class ReactiveConnectionCallback extends CallbackSupport { 18 | 19 | private Connection connection; 20 | private ConnectionInfo connectionInfo; 21 | 22 | public ReactiveConnectionCallback(Connection connection, ConnectionInfo connectionInfo, ProxyConfig proxyConfig) { 23 | super(proxyConfig); 24 | this.connection = connection; 25 | this.connectionInfo = connectionInfo; 26 | } 27 | 28 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 29 | 30 | String methodName = method.getName(); 31 | 32 | if ("getTarget".equals(methodName)) { 33 | return this.connection; 34 | } else if ("getOriginalConnection".equals(methodName)) { 35 | return this.connection; 36 | } 37 | 38 | Consumer onComplete = null; 39 | 40 | // since these methods return Publisher pass the callback for doOnComplete(). 41 | if ("beginTransaction".equals(methodName)) { 42 | onComplete = executionInfo -> { 43 | executionInfo.getConnectionInfo().incrementTransactionCount(); 44 | }; 45 | } else if ("commitTransaction".equals(methodName)) { 46 | onComplete = executionInfo -> { 47 | executionInfo.getConnectionInfo().incrementCommitCount(); 48 | }; 49 | } else if ("rollbackTransaction".equals(methodName)) { 50 | onComplete = executionInfo -> { 51 | executionInfo.getConnectionInfo().incrementRollbackCount(); 52 | }; 53 | } else if ("close".equals(methodName)) { 54 | onComplete = executionInfo -> { 55 | executionInfo.getConnectionInfo().setClosed(true); 56 | }; 57 | } 58 | // TODO: createSavepoint, releaseSavepoint, rollbackTransactionToSavepoint 59 | 60 | Object result = proceedExecution(method, this.connection, args, this.proxyConfig.getListeners(), this.connectionInfo, null, onComplete); 61 | 62 | if ("createBatch".equals(methodName)) { 63 | return this.proxyConfig.getProxyFactory().createBatch((Batch) result, this.connectionInfo); 64 | } else if ("createStatement".equals(methodName)) { 65 | String query = (String) args[0]; 66 | return this.proxyConfig.getProxyFactory().createStatement((Statement) result, query, this.connectionInfo); 67 | } 68 | 69 | return result; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/proxy/ReactiveConnectionFactoryCallback.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Connection; 4 | import io.r2dbc.spi.ConnectionFactory; 5 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 6 | import net.ttddyy.dsproxy.r2dbc.core.MethodExecutionInfo; 7 | 8 | import java.lang.reflect.Method; 9 | import java.util.function.BiFunction; 10 | 11 | /** 12 | * Proxy callback for {@link ConnectionFactory}. 13 | * 14 | * @author Tadaya Tsuyukubo 15 | */ 16 | public class ReactiveConnectionFactoryCallback extends CallbackSupport { 17 | 18 | private ConnectionFactory connectionFactory; 19 | 20 | public ReactiveConnectionFactoryCallback(ConnectionFactory connectionFactory, ProxyConfig proxyConfig) { 21 | super(proxyConfig); 22 | this.connectionFactory = connectionFactory; 23 | } 24 | 25 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 26 | 27 | String methodName = method.getName(); 28 | 29 | if ("getTarget".equals(methodName)) { 30 | return this.connectionFactory; 31 | } 32 | 33 | BiFunction onMap = null; 34 | if ("create".equals(methodName)) { 35 | 36 | // callback for creating connection proxy 37 | onMap = (resultObj, executionInfo) -> { 38 | executionInfo.setResult(resultObj); 39 | 40 | Connection connection = (Connection) resultObj; // original connection 41 | String connectionId = this.proxyConfig.getConnectionIdManager().getId(connection); 42 | 43 | ConnectionInfo connectionInfo = new ConnectionInfo(); 44 | connectionInfo.setConnectionId(connectionId); 45 | connectionInfo.setClosed(false); 46 | connectionInfo.setOriginalConnection(connection); 47 | 48 | executionInfo.setConnectionInfo(connectionInfo); 49 | 50 | Connection proxyConnection = proxyConfig.getProxyFactory().createConnection(connection, connectionInfo); 51 | 52 | return proxyConnection; 53 | }; 54 | 55 | } 56 | 57 | Object result = proceedExecution(method, this.connectionFactory, args, this.proxyConfig.getListeners(), null, onMap, null); 58 | return result; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/proxy/ReactiveResultCallback.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Result; 4 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 5 | import net.ttddyy.dsproxy.r2dbc.core.ProxyEventType; 6 | import net.ttddyy.dsproxy.r2dbc.core.QueryExecutionInfo; 7 | import org.reactivestreams.Publisher; 8 | import reactor.core.publisher.Flux; 9 | 10 | import java.lang.reflect.Method; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | 13 | /** 14 | * Proxy callback for {@link Result}. 15 | * 16 | * @author Tadaya Tsuyukubo 17 | */ 18 | public class ReactiveResultCallback extends CallbackSupport { 19 | 20 | private Result result; 21 | private QueryExecutionInfo queryExecutionInfo; 22 | 23 | public ReactiveResultCallback(Result result, QueryExecutionInfo queryExecutionInfo, ProxyConfig proxyConfig) { 24 | super(proxyConfig); 25 | this.result = result; 26 | this.queryExecutionInfo = queryExecutionInfo; 27 | } 28 | 29 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 30 | 31 | String methodName = method.getName(); 32 | ConnectionInfo connectionInfo = this.queryExecutionInfo.getConnectionInfo(); 33 | 34 | if ("getTarget".equals(methodName)) { // for ProxyObject 35 | return this.result; 36 | } else if ("getOriginalConnection".equals(methodName)) { // for ConnectionHolder 37 | return connectionInfo.getOriginalConnection(); 38 | } 39 | 40 | 41 | Object invocationResult = proceedExecution(method, this.result, args, this.proxyConfig.getListeners(), connectionInfo, null, null); 42 | 43 | if ("map".equals(methodName)) { 44 | 45 | AtomicInteger resultCount = new AtomicInteger(0); 46 | 47 | // add logic to call "listener#eachQueryResult()" 48 | return Flux.from((Publisher) invocationResult) 49 | .doOnEach(signal -> { 50 | 51 | boolean proceed = signal.isOnNext() || signal.isOnError(); 52 | if (!proceed) { 53 | return; 54 | } 55 | 56 | int count = resultCount.incrementAndGet(); 57 | 58 | if (signal.isOnNext()) { 59 | Object mappedResult = signal.get(); 60 | 61 | this.queryExecutionInfo.setCurrentResultCount(count); 62 | this.queryExecutionInfo.setCurrentMappedResult(mappedResult); 63 | this.queryExecutionInfo.setThrowable(null); 64 | } else { 65 | // onError 66 | Throwable thrown = signal.getThrowable(); 67 | this.queryExecutionInfo.setCurrentResultCount(count); 68 | this.queryExecutionInfo.setCurrentMappedResult(null); 69 | this.queryExecutionInfo.setThrowable(thrown); 70 | 71 | } 72 | 73 | this.queryExecutionInfo.setProxyEventType(ProxyEventType.EACH_QUERY_RESULT); 74 | 75 | String threadName = Thread.currentThread().getName(); 76 | long threadId = Thread.currentThread().getId(); 77 | this.queryExecutionInfo.setThreadName(threadName); 78 | this.queryExecutionInfo.setThreadId(threadId); 79 | 80 | // callback 81 | this.proxyConfig.getListeners().eachQueryResult(this.queryExecutionInfo); 82 | 83 | }); 84 | 85 | } 86 | 87 | 88 | return invocationResult; 89 | 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/proxy/ReactiveStatementCallback.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Result; 4 | import io.r2dbc.spi.Statement; 5 | import net.ttddyy.dsproxy.r2dbc.core.BindingValue; 6 | import net.ttddyy.dsproxy.r2dbc.core.BindingValue.NullBindingValue; 7 | import net.ttddyy.dsproxy.r2dbc.core.BindingValue.SimpleBindingValue; 8 | import net.ttddyy.dsproxy.r2dbc.core.Bindings; 9 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 10 | import net.ttddyy.dsproxy.r2dbc.core.ExecutionType; 11 | import net.ttddyy.dsproxy.r2dbc.core.QueryExecutionInfo; 12 | import net.ttddyy.dsproxy.r2dbc.core.QueryInfo; 13 | import org.reactivestreams.Publisher; 14 | 15 | import java.lang.reflect.Method; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.stream.Stream; 19 | 20 | import static java.util.stream.Collectors.toList; 21 | 22 | /** 23 | * Proxy callback for {@link Statement}. 24 | * 25 | * @author Tadaya Tsuyukubo 26 | */ 27 | public class ReactiveStatementCallback extends CallbackSupport { 28 | 29 | private Statement statement; 30 | 31 | private ConnectionInfo connectionInfo; 32 | private String query; 33 | 34 | private List bindings = new ArrayList<>(); 35 | private int currentBindingsIndex = 0; 36 | 37 | public ReactiveStatementCallback(Statement statement, String query, ConnectionInfo connectionInfo, ProxyConfig proxyConfig) { 38 | super(proxyConfig); 39 | this.statement = statement; 40 | this.query = query; 41 | this.connectionInfo = connectionInfo; 42 | } 43 | 44 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 45 | 46 | String methodName = method.getName(); 47 | 48 | if ("getTarget".equals(methodName)) { 49 | return this.statement; 50 | } else if ("getOriginalConnection".equals(methodName)) { 51 | return this.connectionInfo.getOriginalConnection(); 52 | } 53 | 54 | Object result = proceedExecution(method, this.statement, args, this.proxyConfig.getListeners(), this.connectionInfo, null, null); 55 | 56 | // add, bind, bindNull, execute 57 | if ("add".equals(methodName)) { 58 | this.currentBindingsIndex++; 59 | } else if ("bind".equals(methodName) || "bindNull".equals(methodName)) { 60 | 61 | if (this.bindings.size() <= this.currentBindingsIndex) { 62 | this.bindings.add(new Bindings()); 63 | } 64 | Bindings bindings = this.bindings.get(this.currentBindingsIndex); 65 | 66 | BindingValue bindingValue; 67 | if ("bind".equals(methodName)) { 68 | bindingValue = new SimpleBindingValue(args[1]); 69 | } else { 70 | bindingValue = new NullBindingValue((Class) args[1]); 71 | } 72 | 73 | if (args[0] instanceof Integer) { 74 | bindings.addIndexBinding((int) args[0], bindingValue); 75 | } else { 76 | bindings.addIdentifierBinding(args[0], bindingValue); 77 | } 78 | } else if ("execute".equals(methodName)) { 79 | 80 | // build QueryExecutionInfo TODO: improve 81 | QueryInfo queryInfo = new QueryInfo(this.query); 82 | queryInfo.getBindingsList().addAll(this.bindings); 83 | List queries = Stream.of(queryInfo).collect(toList()); 84 | 85 | QueryExecutionInfo execInfo = new QueryExecutionInfo(); 86 | execInfo.setType(ExecutionType.STATEMENT); 87 | execInfo.setQueries(queries); 88 | execInfo.setBindingsSize(this.bindings.size()); 89 | execInfo.setMethod(method); 90 | execInfo.setMethodArgs(args); 91 | execInfo.setConnectionInfo(this.connectionInfo); 92 | 93 | // API defines "execute()" returns a publisher 94 | Publisher publisher = (Publisher) result; 95 | 96 | return interceptQueryExecution(publisher, execInfo); 97 | } 98 | 99 | return result; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/support/LastExecutionAwareListener.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.support; 2 | 3 | import net.ttddyy.dsproxy.r2dbc.core.MethodExecutionInfo; 4 | import net.ttddyy.dsproxy.r2dbc.core.ProxyExecutionListener; 5 | import net.ttddyy.dsproxy.r2dbc.core.QueryExecutionInfo; 6 | 7 | /** 8 | * Keep the last invoked execution. 9 | * 10 | * Used for validating last execution. 11 | * 12 | * @author Tadaya Tsuyukubo 13 | */ 14 | public class LastExecutionAwareListener implements ProxyExecutionListener { 15 | 16 | private QueryExecutionInfo beforeQueryExecutionInfo; 17 | private QueryExecutionInfo afterQueryExecutionInfo; 18 | private QueryExecutionInfo eachQueryResultExecutionInfo; 19 | private MethodExecutionInfo beforeMethodExecutionInfo; 20 | private MethodExecutionInfo afterMethodExecutionInfo; 21 | 22 | @Override 23 | public void beforeQuery(QueryExecutionInfo execInfo) { 24 | this.beforeQueryExecutionInfo = execInfo; 25 | } 26 | 27 | @Override 28 | public void afterQuery(QueryExecutionInfo execInfo) { 29 | this.afterQueryExecutionInfo = execInfo; 30 | } 31 | 32 | @Override 33 | public void eachQueryResult(QueryExecutionInfo execInfo) { 34 | this.eachQueryResultExecutionInfo = execInfo; 35 | } 36 | 37 | @Override 38 | public void beforeMethod(MethodExecutionInfo executionInfo) { 39 | this.beforeMethodExecutionInfo = executionInfo; 40 | } 41 | 42 | @Override 43 | public void afterMethod(MethodExecutionInfo executionInfo) { 44 | this.afterMethodExecutionInfo = executionInfo; 45 | } 46 | 47 | public QueryExecutionInfo getBeforeQueryExecutionInfo() { 48 | return beforeQueryExecutionInfo; 49 | } 50 | 51 | public QueryExecutionInfo getAfterQueryExecutionInfo() { 52 | return afterQueryExecutionInfo; 53 | } 54 | 55 | public QueryExecutionInfo getEachQueryResultExecutionInfo() { 56 | return eachQueryResultExecutionInfo; 57 | } 58 | 59 | public MethodExecutionInfo getBeforeMethodExecutionInfo() { 60 | return beforeMethodExecutionInfo; 61 | } 62 | 63 | public MethodExecutionInfo getAfterMethodExecutionInfo() { 64 | return afterMethodExecutionInfo; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/support/LifeCycleExecutionListener.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.support; 2 | 3 | import io.r2dbc.spi.Batch; 4 | import io.r2dbc.spi.Connection; 5 | import io.r2dbc.spi.ConnectionFactory; 6 | import io.r2dbc.spi.Result; 7 | import io.r2dbc.spi.Statement; 8 | import net.ttddyy.dsproxy.r2dbc.core.ExecutionType; 9 | import net.ttddyy.dsproxy.r2dbc.core.MethodExecutionInfo; 10 | import net.ttddyy.dsproxy.r2dbc.core.ProxyExecutionListener; 11 | import net.ttddyy.dsproxy.r2dbc.core.QueryExecutionInfo; 12 | 13 | import java.lang.reflect.Method; 14 | 15 | /** 16 | * Provides explicit callbacks on all SPI invocations and query executions on given {@link LifeCycleListener}. 17 | * 18 | * @author Tadaya Tsuyukubo 19 | * @see LifeCycleListener 20 | */ 21 | public class LifeCycleExecutionListener implements ProxyExecutionListener { 22 | 23 | private LifeCycleListener delegate; 24 | 25 | public static LifeCycleExecutionListener of(LifeCycleListener lifeCycleListener) { 26 | return new LifeCycleExecutionListener(lifeCycleListener); 27 | } 28 | 29 | public LifeCycleExecutionListener(LifeCycleListener delegate) { 30 | this.delegate = delegate; 31 | } 32 | 33 | @Override 34 | public void beforeMethod(MethodExecutionInfo executionInfo) { 35 | this.delegate.beforeMethod(executionInfo); 36 | methodCallback(executionInfo, true); 37 | } 38 | 39 | @Override 40 | public void afterMethod(MethodExecutionInfo executionInfo) { 41 | methodCallback(executionInfo, false); 42 | this.delegate.afterMethod(executionInfo); 43 | } 44 | 45 | private void methodCallback(MethodExecutionInfo executionInfo, boolean isBefore) { 46 | Method method = executionInfo.getMethod(); 47 | String methodName = method.getName(); 48 | Class methodDeclaringClass = method.getDeclaringClass(); 49 | 50 | 51 | if (ConnectionFactory.class.equals(methodDeclaringClass)) { 52 | // ConnectionFactory methods 53 | if ("create".equals(methodName)) { 54 | if (isBefore) { 55 | this.delegate.beforeCreateOnConnectionFactory(executionInfo); 56 | } else { 57 | this.delegate.afterCreateOnConnectionFactory(executionInfo); 58 | } 59 | } else if ("getMetadata".equals(methodName)) { 60 | if (isBefore) { 61 | this.delegate.beforeGetMetadataOnConnectionFactory(executionInfo); 62 | } else { 63 | this.delegate.afterGetMetadataOnConnectionFactory(executionInfo); 64 | } 65 | } 66 | } else if (Connection.class.equals(methodDeclaringClass)) { 67 | // Connection methods 68 | if ("beginTransaction".equals(methodName)) { 69 | if (isBefore) { 70 | this.delegate.beforeBeginTransactionOnConnection(executionInfo); 71 | } else { 72 | this.delegate.afterBeginTransactionOnConnection(executionInfo); 73 | } 74 | } else if ("close".equals(methodName)) { 75 | if (isBefore) { 76 | this.delegate.beforeCloseOnConnection(executionInfo); 77 | } else { 78 | this.delegate.afterCloseOnConnection(executionInfo); 79 | } 80 | } else if ("commitTransaction".equals(methodName)) { 81 | if (isBefore) { 82 | this.delegate.beforeCommitTransactionOnConnection(executionInfo); 83 | } else { 84 | this.delegate.afterCommitTransactionOnConnection(executionInfo); 85 | } 86 | } else if ("createBatch".equals(methodName)) { 87 | if (isBefore) { 88 | this.delegate.beforeCreateBatchOnConnection(executionInfo); 89 | } else { 90 | this.delegate.afterCreateBatchOnConnection(executionInfo); 91 | } 92 | } else if ("createSavepoint".equals(methodName)) { 93 | if (isBefore) { 94 | this.delegate.beforeCreateSavepointOnConnection(executionInfo); 95 | } else { 96 | this.delegate.afterCreateSavepointOnConnection(executionInfo); 97 | } 98 | } else if ("createStatement".equals(methodName)) { 99 | if (isBefore) { 100 | this.delegate.beforeCreateStatementOnConnection(executionInfo); 101 | } else { 102 | this.delegate.afterCreateStatementOnConnection(executionInfo); 103 | } 104 | } else if ("releaseSavepoint".equals(methodName)) { 105 | if (isBefore) { 106 | this.delegate.beforeReleaseSavepointOnConnection(executionInfo); 107 | } else { 108 | this.delegate.afterReleaseSavepointOnConnection(executionInfo); 109 | } 110 | } else if ("rollbackTransaction".equals(methodName)) { 111 | if (isBefore) { 112 | this.delegate.beforeRollbackTransactionOnConnection(executionInfo); 113 | } else { 114 | this.delegate.afterRollbackTransactionOnConnection(executionInfo); 115 | } 116 | } else if ("rollbackTransactionToSavepoint".equals(methodName)) { 117 | if (isBefore) { 118 | this.delegate.beforeRollbackTransactionToSavepointOnConnection(executionInfo); 119 | } else { 120 | this.delegate.afterRollbackTransactionToSavepointOnConnection(executionInfo); 121 | } 122 | } else if ("setTransactionIsolationLevel".equals(methodName)) { 123 | if (isBefore) { 124 | this.delegate.beforeSetTransactionIsolationLevelOnConnection(executionInfo); 125 | } else { 126 | this.delegate.afterSetTransactionIsolationLevelOnConnection(executionInfo); 127 | } 128 | } 129 | } else if (Batch.class.equals(methodDeclaringClass)) { 130 | // Batch methods 131 | if ("add".equals(methodName)) { 132 | if (isBefore) { 133 | this.delegate.beforeAddOnBatch(executionInfo); 134 | } else { 135 | this.delegate.afterAddOnBatch(executionInfo); 136 | } 137 | } else if ("execute".equals(methodName)) { 138 | if (isBefore) { 139 | this.delegate.beforeExecuteOnBatch(executionInfo); 140 | } else { 141 | this.delegate.afterExecuteOnBatch(executionInfo); 142 | } 143 | } 144 | } else if (Statement.class.equals(methodDeclaringClass)) { 145 | // Statement methods 146 | if ("add".equals(methodName)) { 147 | if (isBefore) { 148 | this.delegate.beforeAddOnStatement(executionInfo); 149 | } else { 150 | this.delegate.afterAddOnStatement(executionInfo); 151 | } 152 | } else if ("bind".equals(methodName)) { 153 | if (isBefore) { 154 | this.delegate.beforeBindOnStatement(executionInfo); 155 | } else { 156 | this.delegate.afterBindOnStatement(executionInfo); 157 | } 158 | } else if ("bindNull".equals(methodName)) { 159 | if (isBefore) { 160 | this.delegate.beforeBindNullOnStatement(executionInfo); 161 | } else { 162 | this.delegate.afterBindNullOnStatement(executionInfo); 163 | } 164 | } else if ("execute".equals(methodName)) { 165 | if (isBefore) { 166 | this.delegate.beforeExecuteOnStatement(executionInfo); 167 | } else { 168 | this.delegate.afterExecuteOnStatement(executionInfo); 169 | } 170 | } 171 | } else if (Result.class.equals(methodDeclaringClass)) { 172 | if ("getRowsUpdated".equals(methodName)) { 173 | if (isBefore) { 174 | this.delegate.beforeGetRowsUpdatedOnResult(executionInfo); 175 | } else { 176 | this.delegate.afterGetRowsUpdatedOnResult(executionInfo); 177 | } 178 | } else if ("map".equals(methodName)) { 179 | if (isBefore) { 180 | this.delegate.beforeMapOnResult(executionInfo); 181 | } else { 182 | this.delegate.afterMapOnResult(executionInfo); 183 | } 184 | } 185 | } 186 | 187 | } 188 | 189 | @Override 190 | public void beforeQuery(QueryExecutionInfo execInfo) { 191 | this.delegate.beforeQuery(execInfo); 192 | queryCallback(execInfo, true); 193 | } 194 | 195 | @Override 196 | public void afterQuery(QueryExecutionInfo execInfo) { 197 | queryCallback(execInfo, false); 198 | this.delegate.afterQuery(execInfo); 199 | } 200 | 201 | private void queryCallback(QueryExecutionInfo execInfo, boolean isBefore) { 202 | ExecutionType executionType = execInfo.getType(); 203 | 204 | if (executionType == ExecutionType.BATCH) { 205 | if (isBefore) { 206 | this.delegate.beforeExecuteOnBatch(execInfo); 207 | } else { 208 | this.delegate.afterExecuteOnBatch(execInfo); 209 | } 210 | } else { 211 | if (isBefore) { 212 | this.delegate.beforeExecuteOnStatement(execInfo); 213 | } else { 214 | this.delegate.afterExecuteOnStatement(execInfo); 215 | } 216 | } 217 | } 218 | 219 | @Override 220 | public void eachQueryResult(QueryExecutionInfo execInfo) { 221 | this.delegate.onEachQueryResult(execInfo); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/support/LifeCycleListener.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.support; 2 | 3 | import net.ttddyy.dsproxy.r2dbc.core.MethodExecutionInfo; 4 | import net.ttddyy.dsproxy.r2dbc.core.QueryExecutionInfo; 5 | 6 | /** 7 | * Provides callback for each SPI. 8 | * 9 | * @author Tadaya Tsuyukubo 10 | * @see LifeCycleExecutionListener 11 | */ 12 | public interface LifeCycleListener { 13 | 14 | // 15 | // for ConnectionFactory 16 | // 17 | 18 | default void beforeCreateOnConnectionFactory(MethodExecutionInfo methodExecutionInfo) { 19 | } 20 | 21 | default void afterCreateOnConnectionFactory(MethodExecutionInfo methodExecutionInfo) { 22 | } 23 | 24 | default void beforeGetMetadataOnConnectionFactory(MethodExecutionInfo methodExecutionInfo) { 25 | } 26 | 27 | default void afterGetMetadataOnConnectionFactory(MethodExecutionInfo methodExecutionInfo) { 28 | } 29 | 30 | // 31 | // for Connection 32 | // 33 | 34 | default void beforeBeginTransactionOnConnection(MethodExecutionInfo methodExecutionInfo) { 35 | } 36 | 37 | default void afterBeginTransactionOnConnection(MethodExecutionInfo methodExecutionInfo) { 38 | } 39 | 40 | default void beforeCloseOnConnection(MethodExecutionInfo methodExecutionInfo) { 41 | } 42 | 43 | default void afterCloseOnConnection(MethodExecutionInfo methodExecutionInfo) { 44 | } 45 | 46 | default void beforeCommitTransactionOnConnection(MethodExecutionInfo methodExecutionInfo) { 47 | } 48 | 49 | default void afterCommitTransactionOnConnection(MethodExecutionInfo methodExecutionInfo) { 50 | } 51 | 52 | default void beforeCreateBatchOnConnection(MethodExecutionInfo methodExecutionInfo) { 53 | } 54 | 55 | default void afterCreateBatchOnConnection(MethodExecutionInfo methodExecutionInfo) { 56 | } 57 | 58 | default void beforeCreateSavepointOnConnection(MethodExecutionInfo methodExecutionInfo) { 59 | } 60 | 61 | default void afterCreateSavepointOnConnection(MethodExecutionInfo methodExecutionInfo) { 62 | } 63 | 64 | default void beforeCreateStatementOnConnection(MethodExecutionInfo methodExecutionInfo) { 65 | } 66 | 67 | default void afterCreateStatementOnConnection(MethodExecutionInfo methodExecutionInfo) { 68 | } 69 | 70 | default void beforeReleaseSavepointOnConnection(MethodExecutionInfo methodExecutionInfo) { 71 | } 72 | 73 | default void afterReleaseSavepointOnConnection(MethodExecutionInfo methodExecutionInfo) { 74 | } 75 | 76 | default void beforeRollbackTransactionOnConnection(MethodExecutionInfo methodExecutionInfo) { 77 | } 78 | 79 | default void afterRollbackTransactionOnConnection(MethodExecutionInfo methodExecutionInfo) { 80 | } 81 | 82 | default void beforeRollbackTransactionToSavepointOnConnection(MethodExecutionInfo methodExecutionInfo) { 83 | } 84 | 85 | default void afterRollbackTransactionToSavepointOnConnection(MethodExecutionInfo methodExecutionInfo) { 86 | } 87 | 88 | default void beforeSetTransactionIsolationLevelOnConnection(MethodExecutionInfo methodExecutionInfo) { 89 | } 90 | 91 | default void afterSetTransactionIsolationLevelOnConnection(MethodExecutionInfo methodExecutionInfo) { 92 | } 93 | 94 | // 95 | // for Batch 96 | // 97 | 98 | default void beforeAddOnBatch(MethodExecutionInfo methodExecutionInfo) { 99 | } 100 | 101 | default void afterAddOnBatch(MethodExecutionInfo methodExecutionInfo) { 102 | } 103 | 104 | default void beforeExecuteOnBatch(MethodExecutionInfo methodExecutionInfo) { 105 | } 106 | 107 | default void afterExecuteOnBatch(MethodExecutionInfo methodExecutionInfo) { 108 | } 109 | 110 | // 111 | // for Statement 112 | // 113 | 114 | default void beforeAddOnStatement(MethodExecutionInfo methodExecutionInfo) { 115 | } 116 | 117 | default void afterAddOnStatement(MethodExecutionInfo methodExecutionInfo) { 118 | } 119 | 120 | default void beforeBindOnStatement(MethodExecutionInfo methodExecutionInfo) { 121 | } 122 | 123 | default void afterBindOnStatement(MethodExecutionInfo methodExecutionInfo) { 124 | } 125 | 126 | default void beforeBindNullOnStatement(MethodExecutionInfo methodExecutionInfo) { 127 | } 128 | 129 | default void afterBindNullOnStatement(MethodExecutionInfo methodExecutionInfo) { 130 | } 131 | 132 | default void beforeExecuteOnStatement(MethodExecutionInfo methodExecutionInfo) { 133 | } 134 | 135 | default void afterExecuteOnStatement(MethodExecutionInfo methodExecutionInfo) { 136 | } 137 | 138 | // 139 | // For Result 140 | // 141 | default void beforeGetRowsUpdatedOnResult(MethodExecutionInfo methodExecutioninfo) { 142 | } 143 | 144 | default void afterGetRowsUpdatedOnResult(MethodExecutionInfo methodExecutioninfo) { 145 | } 146 | 147 | default void beforeMapOnResult(MethodExecutionInfo methodExecutioninfo) { 148 | } 149 | 150 | default void afterMapOnResult(MethodExecutionInfo methodExecutioninfo) { 151 | } 152 | 153 | // 154 | // For query execution 155 | // 156 | 157 | default void beforeExecuteOnBatch(QueryExecutionInfo queryExecutionInfo) { 158 | } 159 | 160 | default void afterExecuteOnBatch(QueryExecutionInfo queryExecutionInfo) { 161 | } 162 | 163 | default void beforeExecuteOnStatement(QueryExecutionInfo queryExecutionInfo) { 164 | } 165 | 166 | default void afterExecuteOnStatement(QueryExecutionInfo queryExecutionInfo) { 167 | } 168 | 169 | // 170 | // processing query result 171 | // 172 | 173 | default void onEachQueryResult(QueryExecutionInfo queryExecutionInfo) { 174 | 175 | } 176 | 177 | // 178 | // For every method 179 | // 180 | 181 | /** 182 | * Called at every method invocation. 183 | * 184 | * When any methods on proxied classes are called, this callback is called first. Then, corresponding 185 | * beforeXxxOnYyy callback will be called. 186 | * 187 | * Analogous to {@link net.ttddyy.dsproxy.r2dbc.core.ProxyExecutionListener#beforeMethod(MethodExecutionInfo)} 188 | * 189 | * @param methodExecutionInfo method execution info 190 | */ 191 | default void beforeMethod(MethodExecutionInfo methodExecutionInfo) { 192 | } 193 | 194 | /** 195 | * Called at every method invocation. 196 | * 197 | * When any methods on proxied classes are called and after actual method is invoked, corresponding 198 | * afterXxxOnYyy callback is called, then this callback method will be invoked. 199 | * 200 | * Analogous to {@link net.ttddyy.dsproxy.r2dbc.core.ProxyExecutionListener#afterMethod(MethodExecutionInfo)} 201 | * 202 | * @param methodExecutionInfo method execution info 203 | */ 204 | default void afterMethod(MethodExecutionInfo methodExecutionInfo) { 205 | } 206 | 207 | // 208 | // For every query 209 | // 210 | 211 | /** 212 | * Called before execution of query. 213 | * 214 | * When query is executed, this callback method is called first, then {@link #beforeExecuteOnStatement(QueryExecutionInfo)} 215 | * or {@link #beforeExecuteOnBatch(QueryExecutionInfo)} will be called. 216 | * 217 | * Analogous to {@link net.ttddyy.dsproxy.r2dbc.core.ProxyExecutionListener#beforeQuery(QueryExecutionInfo)} 218 | * 219 | * @param queryExecutionInfo query execution info 220 | */ 221 | default void beforeQuery(QueryExecutionInfo queryExecutionInfo) { 222 | } 223 | 224 | /** 225 | * Called after execution of query. 226 | * 227 | * When query is executed, after original method is called, then {@link #afterExecuteOnStatement(QueryExecutionInfo)} 228 | * or {@link #afterExecuteOnBatch(QueryExecutionInfo)}, then this method is invoked. 229 | * 230 | * Analogous to {@link net.ttddyy.dsproxy.r2dbc.core.ProxyExecutionListener#afterQuery(QueryExecutionInfo)} 231 | * 232 | * @param queryExecutionInfo query execution info 233 | */ 234 | default void afterQuery(QueryExecutionInfo queryExecutionInfo) { 235 | } 236 | 237 | } 238 | -------------------------------------------------------------------------------- /src/main/java/net/ttddyy/dsproxy/r2dbc/support/MethodExecutionInfoFormatter.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.support; 2 | 3 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 4 | import net.ttddyy.dsproxy.r2dbc.core.MethodExecutionInfo; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.concurrent.atomic.AtomicLong; 9 | import java.util.function.BiConsumer; 10 | import java.util.function.Function; 11 | 12 | /** 13 | * Convert {@link MethodExecutionInfo} to {@link String}. 14 | * 15 | * @author Tadaya Tsuyukubo 16 | */ 17 | public class MethodExecutionInfoFormatter implements Function { 18 | 19 | private static final String DEFAULT_DELIMITER = " "; 20 | 21 | private List> consumers = new ArrayList<>(); 22 | 23 | private AtomicLong sequenceNumber = new AtomicLong(1); 24 | 25 | // Default consumer to format the MethodExecutionInfo 26 | private BiConsumer defaultConsumer = (executionInfo, sb) -> { 27 | long seq = this.sequenceNumber.getAndIncrement(); 28 | long executionTime = executionInfo.getExecuteDuration().toMillis(); 29 | String targetClass = executionInfo.getTarget().getClass().getSimpleName(); 30 | String methodName = executionInfo.getMethod().getName(); 31 | long threadId = executionInfo.getThreadId(); 32 | 33 | ConnectionInfo connectionInfo = executionInfo.getConnectionInfo(); 34 | 35 | String connectionId = (connectionInfo == null || connectionInfo.getConnectionId() == null) ? "n/a" : connectionInfo.getConnectionId(); 36 | 37 | sb.append(String.format("%3d: Thread:%d Connection:%s Time:%d %s#%s()", 38 | seq, threadId, connectionId, executionTime, targetClass, methodName)); 39 | }; 40 | 41 | private String delimiter = DEFAULT_DELIMITER; 42 | 43 | 44 | public static MethodExecutionInfoFormatter withDefault() { 45 | MethodExecutionInfoFormatter formatter = new MethodExecutionInfoFormatter(); 46 | formatter.addConsumer(formatter.defaultConsumer); 47 | return formatter; 48 | } 49 | 50 | @Override 51 | public String apply(MethodExecutionInfo executionInfo) { 52 | return format(executionInfo); 53 | } 54 | 55 | public String format(MethodExecutionInfo executionInfo) { 56 | 57 | StringBuilder sb = new StringBuilder(); 58 | 59 | consumers.forEach(consumer -> { 60 | consumer.accept(executionInfo, sb); 61 | sb.append(this.delimiter); 62 | }); 63 | 64 | chompIfEndWith(sb, this.delimiter); 65 | 66 | return sb.toString(); 67 | 68 | } 69 | 70 | public MethodExecutionInfoFormatter addConsumer(BiConsumer consumer) { 71 | this.consumers.add(consumer); 72 | return this; 73 | } 74 | 75 | // TODO: share this with QueryExecutionInfoFormatter 76 | protected void chompIfEndWith(StringBuilder sb, String s) { 77 | if (sb.length() < s.length()) { 78 | return; 79 | } 80 | final int startIndex = sb.length() - s.length(); 81 | if (sb.substring(startIndex, sb.length()).equals(s)) { 82 | sb.delete(startIndex, sb.length()); 83 | } 84 | } 85 | 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/net/ttddyy/dsproxy/r2dbc/Example.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.ttddyy.dsproxy.r2dbc; 18 | 19 | import io.r2dbc.client.R2dbc; 20 | import io.r2dbc.client.Update; 21 | import io.r2dbc.spi.Result; 22 | import org.junit.jupiter.api.AfterEach; 23 | import org.junit.jupiter.api.BeforeEach; 24 | import org.junit.jupiter.api.Test; 25 | import org.springframework.jdbc.core.JdbcOperations; 26 | import reactor.core.publisher.Flux; 27 | import reactor.core.publisher.Mono; 28 | import reactor.test.StepVerifier; 29 | 30 | import java.util.Arrays; 31 | import java.util.Collections; 32 | import java.util.List; 33 | import java.util.stream.IntStream; 34 | 35 | /** 36 | * COPY from r2dbc-client 37 | * @param 38 | */ 39 | interface Example { 40 | 41 | static Mono> extractColumns(Result result) { 42 | return Flux.from(result 43 | .map((row, rowMetadata) -> row.get("value", Integer.class))) 44 | .collectList(); 45 | } 46 | 47 | static Mono> extractIds(Result result) { 48 | return Flux.from(result 49 | .map((row, rowMetadata) -> row.get("id", Integer.class))) 50 | .collectList(); 51 | } 52 | 53 | @Test 54 | default void batch() { 55 | getJdbcOperations().execute("INSERT INTO test VALUES (100)"); 56 | 57 | getR2dbc() 58 | .withHandle(handle -> handle 59 | 60 | .createBatch() 61 | .add("INSERT INTO test VALUES(200)") 62 | .add("SELECT value FROM test") 63 | .mapResult(Mono::just)) 64 | 65 | .as(StepVerifier::create) 66 | .expectNextCount(2).as("one result for each statement") 67 | .verifyComplete(); 68 | } 69 | 70 | 71 | @Test 72 | default void simpleTransaction() { 73 | getJdbcOperations().execute("INSERT INTO test VALUES (100)"); 74 | 75 | getR2dbc() 76 | .withHandle(handle -> handle 77 | .inTransaction(h1 -> h1 78 | .execute("INSERT INTO test VALUES ($1)", 200))) 79 | .as(StepVerifier::create) 80 | .expectNext(1).as("rows inserted") 81 | .verifyComplete(); 82 | } 83 | 84 | @Test 85 | default void compoundStatement() { 86 | getJdbcOperations().execute("INSERT INTO test VALUES (100)"); 87 | 88 | getR2dbc() 89 | .withHandle(handle -> handle 90 | 91 | .createQuery("SELECT value FROM test; SELECT value FROM test") 92 | .mapResult(Example::extractColumns)) 93 | 94 | .as(StepVerifier::create) 95 | .expectNext(Collections.singletonList(100)).as("value from first statement") 96 | .expectNext(Collections.singletonList(100)).as("value from second statement") 97 | .verifyComplete(); 98 | } 99 | 100 | @BeforeEach 101 | default void createTable() { 102 | getJdbcOperations().execute("CREATE TABLE test ( value INTEGER )"); 103 | } 104 | 105 | @AfterEach 106 | default void dropTable() { 107 | getJdbcOperations().execute("DROP TABLE test"); 108 | } 109 | 110 | /** 111 | * Returns the bind identifier for a given substitution. 112 | * 113 | * @param index the zero-index number of the substitution 114 | * @return the bind identifier for a given substitution 115 | */ 116 | T getIdentifier(int index); 117 | 118 | /** 119 | * Returns a {@link JdbcOperations} for the connected database. 120 | * 121 | * @return a {@link JdbcOperations} for the connected database 122 | */ 123 | JdbcOperations getJdbcOperations(); 124 | 125 | /** 126 | * Returns the database-specific placeholder for a given substitution. 127 | * 128 | * @param index the zero-index number of the substitution 129 | * @return the database-specific placeholder for a given substitution 130 | */ 131 | String getPlaceholder(int index); 132 | 133 | /** 134 | * Returns a {@link R2dbc} for the connected database. 135 | * 136 | * @return a {@link R2dbc} for the connected database 137 | */ 138 | R2dbc getR2dbc(); 139 | 140 | @Test 141 | default void prepareStatement() { 142 | getR2dbc() 143 | .withHandle(handle -> { 144 | Update update = handle.createUpdate(String.format("INSERT INTO test VALUES(%s)", getPlaceholder(0))); 145 | 146 | IntStream.range(0, 10) 147 | .forEach(i -> update.bind(getIdentifier(0), i).add()); 148 | 149 | return update.execute(); 150 | }) 151 | .as(StepVerifier::create) 152 | .expectNextCount(10).as("values from insertions") 153 | .verifyComplete(); 154 | } 155 | 156 | @Test 157 | default void savePoint() { 158 | getJdbcOperations().execute("INSERT INTO test VALUES (100)"); 159 | 160 | getR2dbc() 161 | .withHandle(handle -> handle 162 | .inTransaction(h1 -> h1 163 | .select("SELECT value FROM test") 164 | .mapResult(Example::extractColumns) 165 | 166 | .concatWith(h1.execute(String.format("INSERT INTO test VALUES (%s)", getPlaceholder(0)), 200)) 167 | .concatWith(h1.select("SELECT value FROM test") 168 | .mapResult(Example::extractColumns)) 169 | 170 | .concatWith(h1.createSavepoint("test_savepoint")) 171 | .concatWith(h1.execute(String.format("INSERT INTO test VALUES (%s)", getPlaceholder(0)), 300)) 172 | .concatWith(h1.select("SELECT value FROM test") 173 | .mapResult(Example::extractColumns)) 174 | 175 | .concatWith(h1.rollbackTransactionToSavepoint("test_savepoint")) 176 | .concatWith(h1.select("SELECT value FROM test") 177 | .mapResult(Example::extractColumns)))) 178 | 179 | .as(StepVerifier::create) 180 | .expectNext(Collections.singletonList(100)).as("value from select") 181 | .expectNext(1).as("rows inserted") 182 | .expectNext(Arrays.asList(100, 200)).as("values from select") 183 | .expectNext(1).as("rows inserted") 184 | .expectNext(Arrays.asList(100, 200, 300)).as("values from select") 185 | .expectNext(Arrays.asList(100, 200)).as("values from select") 186 | .verifyComplete(); 187 | } 188 | 189 | @Test 190 | default void transactionCommit() { 191 | getJdbcOperations().execute("INSERT INTO test VALUES (100)"); 192 | 193 | getR2dbc() 194 | .withHandle(handle -> handle 195 | .inTransaction(h1 -> h1 196 | .select("SELECT value FROM test") 197 | .mapResult(Example::extractColumns) 198 | 199 | .concatWith(h1.execute(String.format("INSERT INTO test VALUES (%s)", getPlaceholder(0)), 200)) 200 | .concatWith(h1.select("SELECT value FROM test") 201 | .mapResult(Example::extractColumns))) 202 | 203 | .concatWith(handle.select("SELECT value FROM test") 204 | .mapResult(Example::extractColumns))) 205 | 206 | .as(StepVerifier::create) 207 | .expectNext(Collections.singletonList(100)).as("value from select") 208 | .expectNext(1).as("rows inserted") 209 | .expectNext(Arrays.asList(100, 200)).as("values from select") 210 | .expectNext(Arrays.asList(100, 200)).as("values from select") 211 | .verifyComplete(); 212 | } 213 | 214 | @Test 215 | default void transactionRollback() { 216 | getJdbcOperations().execute("INSERT INTO test VALUES (100)"); 217 | 218 | getR2dbc() 219 | .withHandle(handle -> handle 220 | .inTransaction(h1 -> h1 221 | .select("SELECT value FROM test") 222 | .mapResult(Example::extractColumns) 223 | 224 | .concatWith(h1.execute(String.format("INSERT INTO test VALUES (%s)", getPlaceholder(0)), 200)) 225 | .concatWith(h1.select("SELECT value FROM test") 226 | .mapResult(Example::extractColumns)) 227 | 228 | .concatWith(Mono.error(new Exception()))) 229 | 230 | .onErrorResume(t -> handle.select("SELECT value FROM test") 231 | .mapResult(Example::extractColumns))) 232 | 233 | .as(StepVerifier::create) 234 | .expectNext(Collections.singletonList(100)).as("value from select") 235 | .expectNext(1).as("rows inserted") 236 | .expectNext(Arrays.asList(100, 200)).as("values from select") 237 | .expectNext(Collections.singletonList(100)).as("value from select") 238 | .verifyComplete(); 239 | } 240 | 241 | } 242 | -------------------------------------------------------------------------------- /src/test/java/net/ttddyy/dsproxy/r2dbc/PostgresqlExample.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.ttddyy.dsproxy.r2dbc; 18 | 19 | import com.zaxxer.hikari.HikariDataSource; 20 | import io.r2dbc.client.R2dbc; 21 | import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; 22 | import io.r2dbc.postgresql.PostgresqlConnectionFactory; 23 | import io.r2dbc.spi.ConnectionFactory; 24 | import io.r2dbc.spi.Result; 25 | import net.ttddyy.dsproxy.r2dbc.support.MethodExecutionInfoFormatter; 26 | import net.ttddyy.dsproxy.r2dbc.support.QueryExecutionInfoFormatter; 27 | import org.junit.jupiter.api.extension.AfterAllCallback; 28 | import org.junit.jupiter.api.extension.BeforeAllCallback; 29 | import org.junit.jupiter.api.extension.ExtensionContext; 30 | import org.junit.jupiter.api.extension.RegisterExtension; 31 | import org.springframework.boot.jdbc.DataSourceBuilder; 32 | import org.springframework.jdbc.core.JdbcOperations; 33 | import org.springframework.jdbc.core.JdbcTemplate; 34 | import org.testcontainers.containers.PostgreSQLContainer; 35 | import reactor.util.Logger; 36 | import reactor.util.Loggers; 37 | import reactor.util.annotation.Nullable; 38 | 39 | import java.io.IOException; 40 | 41 | /** 42 | * Copy from r2dbc-client 43 | */ 44 | final class PostgresqlExample implements Example { 45 | 46 | @RegisterExtension 47 | static final PostgresqlServerExtension SERVER = new PostgresqlServerExtension(); 48 | 49 | private final PostgresqlConnectionConfiguration configuration = PostgresqlConnectionConfiguration.builder() 50 | .database(SERVER.getDatabase()) 51 | .host(SERVER.getHost()) 52 | .port(SERVER.getPort()) 53 | .password(SERVER.getPassword()) 54 | .username(SERVER.getUsername()) 55 | .build(); 56 | 57 | // private final R2dbc r2dbc = new R2dbc(new PostgresqlConnectionFactory(this.configuration)); 58 | private R2dbc r2dbc ; 59 | { 60 | ConnectionFactory connectionFactory = new PostgresqlConnectionFactory(this.configuration); 61 | 62 | Logger logger = Loggers.getLogger(getClass()); 63 | 64 | QueryExecutionInfoFormatter queryExecutionFormatter = QueryExecutionInfoFormatter.showAll(); 65 | MethodExecutionInfoFormatter methodExecutionFormatter = MethodExecutionInfoFormatter.withDefault(); 66 | 67 | ConnectionFactory proxyConnectionFactory = 68 | ProxyConnectionFactoryBuilder.create(connectionFactory) 69 | .onAfterMethod(execInfo -> 70 | execInfo.map(methodExecutionFormatter::format) 71 | .doOnNext(System.out::println) 72 | .subscribe()) 73 | .onEachQueryResult(result -> { 74 | result.doOnNext(executionInfo -> { 75 | int currentResultCount = executionInfo.getCurrentResultCount(); 76 | Object currentResult = executionInfo.getCurrentMappedResult(); 77 | 78 | System.out.println("RESULT_NO=" + currentResultCount + " RESULT=" + currentResult); 79 | }).subscribe(); 80 | }) 81 | .onAfterQuery(execInfo -> 82 | execInfo.map(queryExecutionFormatter::format) 83 | .doOnNext(System.out::println) 84 | .subscribe()) 85 | .build(); 86 | 87 | this.r2dbc = new R2dbc(proxyConnectionFactory); 88 | } 89 | 90 | @Override 91 | public String getIdentifier(int index) { 92 | return getPlaceholder(index); 93 | } 94 | 95 | @Override 96 | public JdbcOperations getJdbcOperations() { 97 | JdbcOperations jdbcOperations = SERVER.getJdbcOperations(); 98 | 99 | if (jdbcOperations == null) { 100 | throw new IllegalStateException("JdbcOperations not yet initialized"); 101 | } 102 | 103 | return jdbcOperations; 104 | } 105 | 106 | @Override 107 | public String getPlaceholder(int index) { 108 | return String.format("$%d", index + 1); 109 | } 110 | 111 | @Override 112 | public R2dbc getR2dbc() { 113 | return this.r2dbc; 114 | } 115 | 116 | private static final class PostgresqlServerExtension implements BeforeAllCallback, AfterAllCallback { 117 | 118 | private final PostgreSQLContainer container = new PostgreSQLContainer<>(); 119 | 120 | private HikariDataSource dataSource; 121 | 122 | private JdbcOperations jdbcOperations; 123 | 124 | @Override 125 | public void afterAll(ExtensionContext context) { 126 | this.dataSource.close(); 127 | this.container.stop(); 128 | } 129 | 130 | @Override 131 | public void beforeAll(ExtensionContext context) throws IOException { 132 | this.container.start(); 133 | 134 | this.dataSource = DataSourceBuilder.create() 135 | .type(HikariDataSource.class) 136 | .url(this.container.getJdbcUrl()) 137 | .username(this.container.getUsername()) 138 | .password(this.container.getPassword()) 139 | .build(); 140 | 141 | this.dataSource.setMaximumPoolSize(1); 142 | 143 | this.jdbcOperations = new JdbcTemplate(this.dataSource); 144 | } 145 | 146 | String getDatabase() { 147 | return this.container.getDatabaseName(); 148 | } 149 | 150 | String getHost() { 151 | return this.container.getContainerIpAddress(); 152 | } 153 | 154 | @Nullable 155 | JdbcOperations getJdbcOperations() { 156 | return this.jdbcOperations; 157 | } 158 | 159 | String getPassword() { 160 | return this.container.getPassword(); 161 | } 162 | 163 | int getPort() { 164 | return this.container.getMappedPort(5432); 165 | } 166 | 167 | String getUsername() { 168 | return this.container.getUsername(); 169 | } 170 | 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/test/java/net/ttddyy/dsproxy/r2dbc/core/CompositeProxyExecutionListenerTest.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.core; 2 | 3 | import net.ttddyy.dsproxy.r2dbc.support.LastExecutionAwareListener; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertSame; 8 | 9 | /** 10 | * @author Tadaya Tsuyukubo 11 | */ 12 | public class CompositeProxyExecutionListenerTest { 13 | 14 | private LastExecutionAwareListener listener1; 15 | private LastExecutionAwareListener listener2; 16 | 17 | private CompositeProxyExecutionListener compositeListener; 18 | 19 | @BeforeEach 20 | void setUp() { 21 | this.listener1 = new LastExecutionAwareListener(); 22 | this.listener2 = new LastExecutionAwareListener(); 23 | 24 | this.compositeListener = new CompositeProxyExecutionListener(this.listener1, this.listener2); 25 | } 26 | 27 | @Test 28 | void onQueryExecution() { 29 | 30 | QueryExecutionInfo executionInfo = new QueryExecutionInfo(); 31 | executionInfo.setProxyEventType(ProxyEventType.BEFORE_QUERY); 32 | 33 | this.compositeListener.onQueryExecution(executionInfo); 34 | 35 | assertSame(executionInfo, this.listener1.getBeforeQueryExecutionInfo()); 36 | assertSame(executionInfo, this.listener2.getBeforeQueryExecutionInfo()); 37 | 38 | 39 | executionInfo.setProxyEventType(ProxyEventType.AFTER_QUERY); 40 | this.compositeListener.onQueryExecution(executionInfo); 41 | 42 | assertSame(executionInfo, this.listener1.getAfterQueryExecutionInfo()); 43 | assertSame(executionInfo, this.listener2.getAfterQueryExecutionInfo()); 44 | 45 | } 46 | 47 | @Test 48 | void onMethodExecution() { 49 | 50 | MethodExecutionInfo executionInfo = new MethodExecutionInfo(); 51 | executionInfo.setProxyEventType(ProxyEventType.BEFORE_METHOD); 52 | 53 | this.compositeListener.onMethodExecution(executionInfo); 54 | 55 | assertSame(executionInfo, this.listener1.getBeforeMethodExecutionInfo()); 56 | assertSame(executionInfo, this.listener2.getBeforeMethodExecutionInfo()); 57 | 58 | 59 | executionInfo.setProxyEventType(ProxyEventType.AFTER_METHOD); 60 | this.compositeListener.onMethodExecution(executionInfo); 61 | 62 | assertSame(executionInfo, this.listener1.getAfterMethodExecutionInfo()); 63 | assertSame(executionInfo, this.listener2.getAfterMethodExecutionInfo()); 64 | 65 | } 66 | 67 | @Test 68 | void beforeMethod() { 69 | 70 | MethodExecutionInfo executionInfo = new MethodExecutionInfo(); 71 | executionInfo.setProxyEventType(ProxyEventType.BEFORE_METHOD); 72 | 73 | this.compositeListener.beforeMethod(executionInfo); 74 | 75 | assertSame(executionInfo, this.listener1.getBeforeMethodExecutionInfo()); 76 | assertSame(executionInfo, this.listener2.getBeforeMethodExecutionInfo()); 77 | 78 | } 79 | 80 | @Test 81 | void afterMethod() { 82 | 83 | MethodExecutionInfo executionInfo = new MethodExecutionInfo(); 84 | executionInfo.setProxyEventType(ProxyEventType.AFTER_METHOD); 85 | 86 | this.compositeListener.afterMethod(executionInfo); 87 | 88 | assertSame(executionInfo, this.listener1.getAfterMethodExecutionInfo()); 89 | assertSame(executionInfo, this.listener2.getAfterMethodExecutionInfo()); 90 | 91 | } 92 | 93 | @Test 94 | void beforeQuery() { 95 | 96 | QueryExecutionInfo executionInfo = new QueryExecutionInfo(); 97 | executionInfo.setProxyEventType(ProxyEventType.BEFORE_QUERY); 98 | 99 | this.compositeListener.beforeQuery(executionInfo); 100 | 101 | assertSame(executionInfo, this.listener1.getBeforeQueryExecutionInfo()); 102 | assertSame(executionInfo, this.listener2.getBeforeQueryExecutionInfo()); 103 | 104 | } 105 | 106 | @Test 107 | void afterQuery() { 108 | 109 | QueryExecutionInfo executionInfo = new QueryExecutionInfo(); 110 | executionInfo.setProxyEventType(ProxyEventType.AFTER_QUERY); 111 | 112 | this.compositeListener.afterQuery(executionInfo); 113 | 114 | assertSame(executionInfo, this.listener1.getAfterQueryExecutionInfo()); 115 | assertSame(executionInfo, this.listener2.getAfterQueryExecutionInfo()); 116 | 117 | } 118 | 119 | @Test 120 | void eachQueryResult() { 121 | 122 | QueryExecutionInfo executionInfo = new QueryExecutionInfo(); 123 | executionInfo.setProxyEventType(ProxyEventType.EACH_QUERY_RESULT); 124 | 125 | this.compositeListener.eachQueryResult(executionInfo); 126 | 127 | assertSame(executionInfo, this.listener1.getEachQueryResultExecutionInfo()); 128 | assertSame(executionInfo, this.listener2.getEachQueryResultExecutionInfo()); 129 | 130 | } 131 | 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/test/java/net/ttddyy/dsproxy/r2dbc/proxy/JdkProxyFactoryTest.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Batch; 4 | import io.r2dbc.spi.Connection; 5 | import io.r2dbc.spi.ConnectionFactory; 6 | import io.r2dbc.spi.Statement; 7 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionHolder; 8 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import java.lang.reflect.Proxy; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.junit.jupiter.api.Assertions.assertTrue; 17 | import static org.mockito.Mockito.mock; 18 | 19 | /** 20 | * @author Tadaya Tsuyukubo 21 | */ 22 | public class JdkProxyFactoryTest { 23 | 24 | private JdkProxyFactory proxyFactory; 25 | 26 | @BeforeEach 27 | void setUp() { 28 | this.proxyFactory = new JdkProxyFactory(); 29 | this.proxyFactory.setProxyConfig(new ProxyConfig()); 30 | } 31 | 32 | @Test 33 | void isProxy() { 34 | ConnectionFactory connectionFactory = mock(ConnectionFactory.class); 35 | Connection connection = mock(Connection.class); 36 | Batch batch = mock(Batch.class); 37 | Statement statement = mock(Statement.class); 38 | ConnectionInfo connectionInfo = new ConnectionInfo(); 39 | String query = "query"; 40 | 41 | Object result; 42 | 43 | result = this.proxyFactory.createConnectionFactory(connectionFactory); 44 | assertTrue(Proxy.isProxyClass(result.getClass())); 45 | assertThat(result).isInstanceOf(ProxyObject.class); 46 | assertThat(result).isNotInstanceOf(ConnectionHolder.class); 47 | 48 | result = this.proxyFactory.createConnection(connection, connectionInfo); 49 | assertTrue(Proxy.isProxyClass(result.getClass())); 50 | assertThat(result).isInstanceOf(ProxyObject.class); 51 | assertThat(result).isInstanceOf(ConnectionHolder.class); 52 | 53 | result = this.proxyFactory.createBatch(batch, connectionInfo); 54 | assertTrue(Proxy.isProxyClass(result.getClass())); 55 | assertThat(result).isInstanceOf(ProxyObject.class); 56 | assertThat(result).isInstanceOf(ConnectionHolder.class); 57 | 58 | result = this.proxyFactory.createStatement(statement, query, connectionInfo); 59 | assertTrue(Proxy.isProxyClass(result.getClass())); 60 | assertThat(result).isInstanceOf(ProxyObject.class); 61 | assertThat(result).isInstanceOf(ConnectionHolder.class); 62 | } 63 | 64 | @Test 65 | void testToString() { 66 | ConnectionFactory connectionFactory = mock(ConnectionFactory.class); 67 | Connection connection = mock(Connection.class); 68 | Batch batch = mock(Batch.class); 69 | Statement statement = mock(Statement.class); 70 | ConnectionInfo connectionInfo = new ConnectionInfo(); 71 | String query = "query"; 72 | 73 | String expected; 74 | Object result; 75 | 76 | result = this.proxyFactory.createConnectionFactory(connectionFactory); 77 | expected = getExpectedToString(connectionFactory); 78 | assertEquals(expected, result.toString()); 79 | 80 | result = this.proxyFactory.createConnection(connection, connectionInfo); 81 | expected = getExpectedToString(connection); 82 | assertEquals(expected, result.toString()); 83 | 84 | result = this.proxyFactory.createBatch(batch, connectionInfo); 85 | expected = getExpectedToString(batch); 86 | assertEquals(expected, result.toString()); 87 | 88 | result = this.proxyFactory.createStatement(statement, query, connectionInfo); 89 | expected = getExpectedToString(statement); 90 | assertEquals(expected, result.toString()); 91 | 92 | } 93 | 94 | private String getExpectedToString(Object target) { 95 | return target.getClass().getSimpleName() + "-proxy [" + target.toString() + "]"; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/net/ttddyy/dsproxy/r2dbc/proxy/ProxyUtilsTest.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Batch; 4 | import io.r2dbc.spi.Connection; 5 | import io.r2dbc.spi.Result; 6 | import io.r2dbc.spi.Statement; 7 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 8 | import net.ttddyy.dsproxy.r2dbc.core.QueryExecutionInfo; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertSame; 12 | import static org.mockito.Mockito.mock; 13 | 14 | /** 15 | * @author Tadaya Tsuyukubo 16 | */ 17 | public class ProxyUtilsTest { 18 | 19 | @Test 20 | void getOriginalConnection() { 21 | Connection originalConnection = mock(Connection.class); 22 | Batch originalBatch = mock(Batch.class); 23 | Statement originalStatement = mock(Statement.class); 24 | Result originalResult = mock(Result.class); 25 | 26 | String query = "QUERY"; 27 | 28 | ProxyConfig proxyConfig = new ProxyConfig(); 29 | proxyConfig.setProxyFactory(new JdkProxyFactory()); 30 | 31 | ConnectionInfo connectionInfo = new ConnectionInfo(); 32 | 33 | Connection proxyConnection = proxyConfig.getProxyFactory().createConnection(originalConnection, connectionInfo); 34 | connectionInfo.setOriginalConnection(originalConnection); 35 | 36 | QueryExecutionInfo queryExecutionInfo = new QueryExecutionInfo(); 37 | queryExecutionInfo.setConnectionInfo(connectionInfo); 38 | 39 | Batch proxyBatch = proxyConfig.getProxyFactory().createBatch(originalBatch, connectionInfo); 40 | Statement proxyStatement = proxyConfig.getProxyFactory().createStatement(originalStatement, query, connectionInfo); 41 | Result proxyResult = proxyConfig.getProxyFactory().createResult(originalResult, queryExecutionInfo); 42 | 43 | Connection result; 44 | 45 | result = ProxyUtils.getOriginalConnection(proxyConnection); 46 | assertSame(originalConnection, result); 47 | 48 | result = ProxyUtils.getOriginalConnection(proxyBatch); 49 | assertSame(originalConnection, result); 50 | 51 | result = ProxyUtils.getOriginalConnection(proxyStatement); 52 | assertSame(originalConnection, result); 53 | 54 | result = ProxyUtils.getOriginalConnection(proxyResult); 55 | assertSame(originalConnection, result); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/net/ttddyy/dsproxy/r2dbc/proxy/ReactiveBatchCallbackTest.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Batch; 4 | import io.r2dbc.spi.Result; 5 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 6 | import net.ttddyy.dsproxy.r2dbc.core.ExecutionType; 7 | import net.ttddyy.dsproxy.r2dbc.support.LastExecutionAwareListener; 8 | import net.ttddyy.dsproxy.r2dbc.core.QueryExecutionInfo; 9 | import net.ttddyy.dsproxy.r2dbc.core.QueryInfo; 10 | import org.junit.jupiter.api.Test; 11 | import org.reactivestreams.Publisher; 12 | import org.springframework.util.ReflectionUtils; 13 | import reactor.core.publisher.Flux; 14 | import reactor.test.StepVerifier; 15 | 16 | import java.lang.reflect.Method; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.junit.jupiter.api.Assertions.assertSame; 20 | import static org.mockito.Mockito.mock; 21 | import static org.mockito.Mockito.when; 22 | 23 | /** 24 | * @author Tadaya Tsuyukubo 25 | */ 26 | public class ReactiveBatchCallbackTest { 27 | 28 | private static Method ADD_METHOD = ReflectionUtils.findMethod(Batch.class, "add", String.class); 29 | private static Method EXECUTE_METHOD = ReflectionUtils.findMethod(Batch.class, "execute"); 30 | private static Method GET_TARGET_METHOD = ReflectionUtils.findMethod(ProxyObject.class, "getTarget"); 31 | 32 | @Test 33 | void batchOperation() throws Throwable { 34 | LastExecutionAwareListener testListener = new LastExecutionAwareListener(); 35 | 36 | ConnectionInfo connectionInfo = new ConnectionInfo(); 37 | ProxyConfig proxyConfig = new ProxyConfig(); 38 | proxyConfig.addListener(testListener); 39 | Batch batch = mock(Batch.class); 40 | ReactiveBatchCallback callback = new ReactiveBatchCallback(batch, connectionInfo, proxyConfig); 41 | 42 | // mock batch execution 43 | when(batch.execute()).thenReturn(Flux.empty()); 44 | 45 | String query1 = "QUERY-1"; 46 | String query2 = "QUERY-2"; 47 | 48 | callback.invoke(null, ADD_METHOD, new String[]{query1}); 49 | callback.invoke(null, ADD_METHOD, new String[]{query2}); 50 | 51 | Object result = callback.invoke(null, EXECUTE_METHOD, new String[]{}); 52 | 53 | 54 | StepVerifier.create((Publisher) result) 55 | .verifyComplete(); 56 | 57 | QueryExecutionInfo beforeQueryInfo = testListener.getBeforeQueryExecutionInfo(); 58 | QueryExecutionInfo afterQueryInfo = testListener.getAfterQueryExecutionInfo(); 59 | 60 | assertThat(beforeQueryInfo).isNotNull(); 61 | assertThat(beforeQueryInfo.getBatchSize()).isEqualTo(2); 62 | assertThat(beforeQueryInfo.getBindingsSize()).isEqualTo(0); 63 | assertThat(beforeQueryInfo.isSuccess()).isTrue(); 64 | assertThat(beforeQueryInfo.getType()).isEqualTo(ExecutionType.BATCH); 65 | assertThat(beforeQueryInfo.getConnectionInfo()).isSameAs(connectionInfo); 66 | assertThat(beforeQueryInfo.getQueries()) 67 | .extracting(QueryInfo::getQuery) 68 | .containsExactly("QUERY-1", "QUERY-2"); 69 | 70 | assertThat(afterQueryInfo).isNotNull(); 71 | assertThat(afterQueryInfo.getBatchSize()).isEqualTo(2); 72 | assertThat(afterQueryInfo.getBindingsSize()).isEqualTo(0); 73 | assertThat(afterQueryInfo.isSuccess()).isTrue(); 74 | assertThat(afterQueryInfo.getType()).isEqualTo(ExecutionType.BATCH); 75 | assertThat(afterQueryInfo.getConnectionInfo()).isSameAs(connectionInfo); 76 | assertThat(afterQueryInfo.getQueries()) 77 | .extracting(QueryInfo::getQuery) 78 | .containsExactly("QUERY-1", "QUERY-2"); 79 | 80 | } 81 | 82 | @Test 83 | void getTarget() throws Throwable { 84 | Batch batch = mock(Batch.class); 85 | ConnectionInfo connectionInfo = new ConnectionInfo(); 86 | ProxyConfig proxyConfig = new ProxyConfig(); 87 | 88 | ReactiveBatchCallback callback = new ReactiveBatchCallback(batch, connectionInfo, proxyConfig); 89 | 90 | Object result = callback.invoke(null, GET_TARGET_METHOD, null); 91 | assertSame(batch, result); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/net/ttddyy/dsproxy/r2dbc/proxy/ReactiveConnectionCallbackTest.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Batch; 4 | import io.r2dbc.spi.Connection; 5 | import io.r2dbc.spi.Statement; 6 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 7 | import net.ttddyy.dsproxy.r2dbc.support.LastExecutionAwareListener; 8 | import net.ttddyy.dsproxy.r2dbc.core.MethodExecutionInfo; 9 | import org.junit.jupiter.api.Test; 10 | import org.reactivestreams.Publisher; 11 | import org.springframework.util.ReflectionUtils; 12 | import reactor.core.publisher.Mono; 13 | import reactor.test.StepVerifier; 14 | 15 | import java.lang.reflect.Method; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.junit.jupiter.api.Assertions.assertSame; 19 | import static org.junit.jupiter.api.Assertions.assertTrue; 20 | import static org.mockito.Mockito.mock; 21 | import static org.mockito.Mockito.when; 22 | 23 | /** 24 | * @author Tadaya Tsuyukubo 25 | */ 26 | public class ReactiveConnectionCallbackTest { 27 | 28 | private static Method CREATE_BATCH_METHOD = ReflectionUtils.findMethod(Connection.class, "createBatch"); 29 | private static Method CREATE_STATEMENT_METHOD = ReflectionUtils.findMethod(Connection.class, "createStatement", String.class); 30 | private static Method BEGIN_TRANSACTION_METHOD = ReflectionUtils.findMethod(Connection.class, "beginTransaction"); 31 | private static Method COMMIT_TRANSACTION_METHOD = ReflectionUtils.findMethod(Connection.class, "commitTransaction"); 32 | private static Method ROLLBACK_TRANSACTION_METHOD = ReflectionUtils.findMethod(Connection.class, "rollbackTransaction"); 33 | private static Method CLOSE_METHOD = ReflectionUtils.findMethod(Connection.class, "close"); 34 | private static Method GET_TARGET_METHOD = ReflectionUtils.findMethod(ProxyObject.class, "getTarget"); 35 | 36 | @Test 37 | void createBatch() throws Throwable { 38 | LastExecutionAwareListener listener = new LastExecutionAwareListener(); 39 | 40 | Connection connection = mock(Connection.class); 41 | ConnectionInfo connectionInfo = new ConnectionInfo(); 42 | 43 | ProxyFactory proxyFactory = mock(ProxyFactory.class); 44 | 45 | ProxyConfig proxyConfig = new ProxyConfig(); 46 | proxyConfig.addListener(listener); 47 | proxyConfig.setProxyFactory(proxyFactory); 48 | 49 | Batch originalBatch = mock(Batch.class); 50 | Batch resultBatch = mock(Batch.class); 51 | when(connection.createBatch()).thenReturn(originalBatch); 52 | 53 | when(proxyFactory.createBatch(originalBatch, connectionInfo)).thenReturn(resultBatch); 54 | 55 | ReactiveConnectionCallback callback = new ReactiveConnectionCallback(connection, connectionInfo, proxyConfig); 56 | 57 | Object result = callback.invoke(null, CREATE_BATCH_METHOD, null); 58 | 59 | assertSame(resultBatch, result); 60 | 61 | MethodExecutionInfo executionInfo = listener.getAfterMethodExecutionInfo(); 62 | assertEquals(originalBatch, executionInfo.getResult()); 63 | } 64 | 65 | @Test 66 | void createStatement() throws Throwable { 67 | LastExecutionAwareListener listener = new LastExecutionAwareListener(); 68 | ProxyFactory proxyFactory = mock(ProxyFactory.class); 69 | 70 | Connection connection = mock(Connection.class); 71 | ConnectionInfo connectionInfo = new ConnectionInfo(); 72 | 73 | String query = "MY-QUERY"; 74 | 75 | ProxyConfig proxyConfig = new ProxyConfig(); 76 | proxyConfig.addListener(listener); 77 | proxyConfig.setProxyFactory(proxyFactory); 78 | 79 | Statement originalStatement = mock(Statement.class); 80 | Statement resultStatement = mock(Statement.class); 81 | when(connection.createStatement(query)).thenReturn(originalStatement); 82 | 83 | when(proxyFactory.createStatement(originalStatement, query, connectionInfo)).thenReturn(resultStatement); 84 | 85 | ReactiveConnectionCallback callback = new ReactiveConnectionCallback(connection, connectionInfo, proxyConfig); 86 | 87 | Object result = callback.invoke(null, CREATE_STATEMENT_METHOD, new Object[]{query}); 88 | 89 | assertSame(resultStatement, result); 90 | 91 | MethodExecutionInfo executionInfo = listener.getAfterMethodExecutionInfo(); 92 | assertEquals(originalStatement, executionInfo.getResult()); 93 | } 94 | 95 | @Test 96 | void beginTransaction() throws Throwable { 97 | LastExecutionAwareListener listener = new LastExecutionAwareListener(); 98 | 99 | Connection connection = mock(Connection.class); 100 | ConnectionInfo connectionInfo = new ConnectionInfo(); 101 | ProxyConfig proxyConfig = new ProxyConfig(); 102 | proxyConfig.addListener(listener); 103 | 104 | when(connection.beginTransaction()).thenReturn(Mono.empty()); 105 | 106 | ReactiveConnectionCallback callback = new ReactiveConnectionCallback(connection, connectionInfo, proxyConfig); 107 | 108 | Object result = callback.invoke(null, BEGIN_TRANSACTION_METHOD, null); 109 | 110 | StepVerifier.create((Publisher) result) 111 | .expectSubscription() 112 | // since it is a Publisher, no steps for assertNext 113 | .verifyComplete(); 114 | 115 | MethodExecutionInfo executionInfo = listener.getAfterMethodExecutionInfo(); 116 | assertSame(connectionInfo, executionInfo.getConnectionInfo()); 117 | 118 | assertEquals(1, connectionInfo.getTransactionCount()); 119 | } 120 | 121 | @Test 122 | void commitTransaction() throws Throwable { 123 | LastExecutionAwareListener listener = new LastExecutionAwareListener(); 124 | 125 | Connection connection = mock(Connection.class); 126 | ConnectionInfo connectionInfo = new ConnectionInfo(); 127 | ProxyConfig proxyConfig = new ProxyConfig(); 128 | proxyConfig.addListener(listener); 129 | 130 | when(connection.commitTransaction()).thenReturn(Mono.empty()); 131 | 132 | ReactiveConnectionCallback callback = new ReactiveConnectionCallback(connection, connectionInfo, proxyConfig); 133 | 134 | Object result = callback.invoke(null, COMMIT_TRANSACTION_METHOD, null); 135 | 136 | StepVerifier.create((Publisher) result) 137 | .expectSubscription() 138 | // since it is a Publisher, no steps for assertNext 139 | .verifyComplete(); 140 | 141 | MethodExecutionInfo executionInfo = listener.getAfterMethodExecutionInfo(); 142 | assertSame(connectionInfo, executionInfo.getConnectionInfo()); 143 | 144 | assertEquals(1, connectionInfo.getCommitCount()); 145 | } 146 | 147 | @Test 148 | void rollbackTransaction() throws Throwable { 149 | LastExecutionAwareListener listener = new LastExecutionAwareListener(); 150 | 151 | Connection connection = mock(Connection.class); 152 | ConnectionInfo connectionInfo = new ConnectionInfo(); 153 | ProxyConfig proxyConfig = new ProxyConfig(); 154 | proxyConfig.addListener(listener); 155 | 156 | when(connection.rollbackTransaction()).thenReturn(Mono.empty()); 157 | 158 | ReactiveConnectionCallback callback = new ReactiveConnectionCallback(connection, connectionInfo, proxyConfig); 159 | 160 | Object result = callback.invoke(null, ROLLBACK_TRANSACTION_METHOD, null); 161 | 162 | StepVerifier.create((Publisher) result) 163 | .expectSubscription() 164 | // since it is a Publisher, no steps for assertNext 165 | .verifyComplete(); 166 | 167 | MethodExecutionInfo executionInfo = listener.getAfterMethodExecutionInfo(); 168 | assertSame(connectionInfo, executionInfo.getConnectionInfo()); 169 | 170 | assertEquals(1, connectionInfo.getRollbackCount()); 171 | } 172 | 173 | @Test 174 | void close() throws Throwable { 175 | LastExecutionAwareListener listener = new LastExecutionAwareListener(); 176 | 177 | Connection connection = mock(Connection.class); 178 | ConnectionInfo connectionInfo = new ConnectionInfo(); 179 | ProxyConfig proxyConfig = new ProxyConfig(); 180 | proxyConfig.addListener(listener); 181 | 182 | when(connection.close()).thenReturn(Mono.empty()); 183 | 184 | ReactiveConnectionCallback callback = new ReactiveConnectionCallback(connection, connectionInfo, proxyConfig); 185 | 186 | Object result = callback.invoke(null, CLOSE_METHOD, null); 187 | 188 | StepVerifier.create((Publisher) result) 189 | .expectSubscription() 190 | // since it is a Publisher, no steps for assertNext 191 | .verifyComplete(); 192 | 193 | MethodExecutionInfo executionInfo = listener.getAfterMethodExecutionInfo(); 194 | assertSame(connectionInfo, executionInfo.getConnectionInfo()); 195 | 196 | assertTrue(connectionInfo.isClosed()); 197 | } 198 | 199 | @Test 200 | void getTarget() throws Throwable { 201 | Connection connection = mock(Connection.class); 202 | ConnectionInfo connectionInfo = new ConnectionInfo(); 203 | ProxyConfig proxyConfig = new ProxyConfig(); 204 | 205 | ReactiveConnectionCallback callback = new ReactiveConnectionCallback(connection, connectionInfo, proxyConfig); 206 | 207 | Object result = callback.invoke(null, GET_TARGET_METHOD, null); 208 | assertSame(connection, result); 209 | } 210 | 211 | } 212 | -------------------------------------------------------------------------------- /src/test/java/net/ttddyy/dsproxy/r2dbc/proxy/ReactiveConnectionFactoryCallbackTest.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Connection; 4 | import io.r2dbc.spi.ConnectionFactory; 5 | import io.r2dbc.spi.ConnectionFactoryMetadata; 6 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionIdManager; 7 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 8 | import net.ttddyy.dsproxy.r2dbc.support.LastExecutionAwareListener; 9 | import net.ttddyy.dsproxy.r2dbc.core.MethodExecutionInfo; 10 | import org.junit.jupiter.api.Test; 11 | import org.reactivestreams.Publisher; 12 | import org.springframework.util.ReflectionUtils; 13 | import reactor.core.publisher.Mono; 14 | import reactor.test.StepVerifier; 15 | 16 | import java.lang.reflect.Method; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.junit.jupiter.api.Assertions.assertEquals; 20 | import static org.junit.jupiter.api.Assertions.assertFalse; 21 | import static org.junit.jupiter.api.Assertions.assertNotNull; 22 | import static org.junit.jupiter.api.Assertions.assertSame; 23 | import static org.mockito.ArgumentMatchers.any; 24 | import static org.mockito.Mockito.doReturn; 25 | import static org.mockito.Mockito.mock; 26 | import static org.mockito.Mockito.when; 27 | 28 | /** 29 | * @author Tadaya Tsuyukubo 30 | */ 31 | public class ReactiveConnectionFactoryCallbackTest { 32 | 33 | private static Method CREATE_METHOD = ReflectionUtils.findMethod(ConnectionFactory.class, "create"); 34 | private static Method GET_METADATA_METHOD = ReflectionUtils.findMethod(ConnectionFactory.class, "getMetadata"); 35 | private static Method GET_TARGET_METHOD = ReflectionUtils.findMethod(ProxyObject.class, "getTarget"); 36 | 37 | @Test 38 | void createConnection() throws Throwable { 39 | 40 | ConnectionFactory connectionFactory = mock(ConnectionFactory.class); 41 | Connection originalConnection = mock(Connection.class); 42 | Connection mockedConnection = mock(Connection.class); 43 | ConnectionIdManager idManager = mock(ConnectionIdManager.class); 44 | ProxyFactory proxyFactory = mock(ProxyFactory.class); 45 | 46 | LastExecutionAwareListener listener = new LastExecutionAwareListener(); 47 | 48 | // mock call to original ConnectionFactory#create() 49 | doReturn(Mono.just(originalConnection)).when(connectionFactory).create(); 50 | 51 | String connectionId = "100"; 52 | when(idManager.getId(originalConnection)).thenReturn(connectionId); 53 | 54 | // mock where it creates proxied connection 55 | when(proxyFactory.createConnection(any(Connection.class), any(ConnectionInfo.class))).thenReturn(mockedConnection); 56 | 57 | 58 | ProxyConfig proxyConfig = new ProxyConfig(); 59 | proxyConfig.setConnectionIdManager(idManager); 60 | proxyConfig.setProxyFactory(proxyFactory); 61 | proxyConfig.addListener(listener); 62 | 63 | ReactiveConnectionFactoryCallback callback = new ReactiveConnectionFactoryCallback(connectionFactory, proxyConfig); 64 | 65 | Object result = callback.invoke(null, CREATE_METHOD, null); 66 | 67 | assertThat(result).isInstanceOf(Publisher.class); 68 | 69 | StepVerifier.create((Publisher) result) 70 | .expectSubscription() 71 | .assertNext(object -> { 72 | assertSame(mockedConnection, object); 73 | }) 74 | .verifyComplete(); 75 | 76 | MethodExecutionInfo afterMethod = listener.getAfterMethodExecutionInfo(); 77 | assertNotNull(afterMethod); 78 | ConnectionInfo connectionInfo = afterMethod.getConnectionInfo(); 79 | assertNotNull(connectionInfo); 80 | assertEquals(connectionId, connectionInfo.getConnectionId()); 81 | assertFalse(connectionInfo.isClosed()); 82 | assertSame(originalConnection, connectionInfo.getOriginalConnection()); 83 | 84 | assertSame(connectionFactory, afterMethod.getTarget()); 85 | assertSame(originalConnection, afterMethod.getResult()); 86 | } 87 | 88 | @Test 89 | void getMetadata() throws Throwable { 90 | 91 | ConnectionFactory connectionFactory = mock(ConnectionFactory.class); 92 | ConnectionFactoryMetadata metadata = mock(ConnectionFactoryMetadata.class); 93 | 94 | when(connectionFactory.getMetadata()).thenReturn(metadata); 95 | 96 | ProxyConfig proxyConfig = new ProxyConfig(); 97 | ReactiveConnectionFactoryCallback callback = new ReactiveConnectionFactoryCallback(connectionFactory, proxyConfig); 98 | 99 | Object result = callback.invoke(null, GET_METADATA_METHOD, null); 100 | 101 | assertSame(metadata, result); 102 | 103 | } 104 | 105 | @Test 106 | void getTarget() throws Throwable { 107 | ConnectionFactory connectionFactory = mock(ConnectionFactory.class); 108 | ProxyConfig proxyConfig = new ProxyConfig(); 109 | 110 | ReactiveConnectionFactoryCallback callback = new ReactiveConnectionFactoryCallback(connectionFactory, proxyConfig); 111 | 112 | Object result = callback.invoke(null, GET_TARGET_METHOD, null); 113 | assertSame(connectionFactory, result); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/test/java/net/ttddyy/dsproxy/r2dbc/proxy/ReactiveResultCallbackTest.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Result; 4 | import net.ttddyy.dsproxy.r2dbc.core.CompositeProxyExecutionListener; 5 | import net.ttddyy.dsproxy.r2dbc.core.ProxyEventType; 6 | import net.ttddyy.dsproxy.r2dbc.core.QueryExecutionInfo; 7 | import net.ttddyy.dsproxy.r2dbc.support.LastExecutionAwareListener; 8 | import org.junit.jupiter.api.Test; 9 | import org.reactivestreams.Publisher; 10 | import org.springframework.util.ReflectionUtils; 11 | import reactor.core.publisher.Flux; 12 | import reactor.test.StepVerifier; 13 | import reactor.test.publisher.TestPublisher; 14 | 15 | import java.lang.reflect.Method; 16 | import java.util.function.BiFunction; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.junit.jupiter.api.Assertions.assertEquals; 20 | import static org.junit.jupiter.api.Assertions.assertNotSame; 21 | import static org.junit.jupiter.api.Assertions.assertNull; 22 | import static org.junit.jupiter.api.Assertions.assertSame; 23 | import static org.mockito.ArgumentMatchers.any; 24 | import static org.mockito.Mockito.mock; 25 | import static org.mockito.Mockito.when; 26 | 27 | /** 28 | * @author Tadaya Tsuyukubo 29 | */ 30 | public class ReactiveResultCallbackTest { 31 | 32 | private static Method MAP_METHOD = ReflectionUtils.findMethod(Result.class, "map", BiFunction.class); 33 | private static Method GET_TARGET_METHOD = ReflectionUtils.findMethod(ProxyObject.class, "getTarget"); 34 | 35 | @Test 36 | void map() throws Throwable { 37 | LastExecutionAwareListener listener = new LastExecutionAwareListener(); 38 | CompositeProxyExecutionListener compositeListener = new CompositeProxyExecutionListener(listener); 39 | 40 | QueryExecutionInfo queryExecutionInfo = new QueryExecutionInfo(); 41 | ProxyConfig proxyConfig = new ProxyConfig(); 42 | proxyConfig.addListener(compositeListener); 43 | 44 | Publisher source = Flux.just("foo", "bar", "baz"); 45 | Result mockResult = mock(Result.class); 46 | when(mockResult.map(any())).thenReturn(source); 47 | 48 | ReactiveResultCallback callback = new ReactiveResultCallback(mockResult, queryExecutionInfo, proxyConfig); 49 | 50 | // since "mockResult.map()" is mocked, args can be anything as long as num of args matches to signature. 51 | Object[] args = new Object[]{null}; 52 | Object result = callback.invoke(null, MAP_METHOD, args); 53 | 54 | assertThat(result) 55 | .isInstanceOf(Publisher.class) 56 | .isNotSameAs(source); 57 | 58 | long threadId = Thread.currentThread().getId(); 59 | String threadName = Thread.currentThread().getName(); 60 | 61 | StepVerifier.create((Publisher) result) 62 | .expectSubscription() 63 | .assertNext(obj -> { // first 64 | assertEquals("foo", obj); 65 | assertSame(queryExecutionInfo, listener.getEachQueryResultExecutionInfo()); 66 | 67 | // verify EACH_QUERY_RESULT 68 | assertEquals(ProxyEventType.EACH_QUERY_RESULT, queryExecutionInfo.getProxyEventType()); 69 | assertEquals(1, queryExecutionInfo.getCurrentResultCount()); 70 | assertEquals("foo", queryExecutionInfo.getCurrentMappedResult()); 71 | assertEquals(threadId, queryExecutionInfo.getThreadId()); 72 | assertEquals(threadName, queryExecutionInfo.getThreadName()); 73 | assertNull(queryExecutionInfo.getThrowable()); 74 | }) 75 | .assertNext(obj -> { // second 76 | assertEquals("bar", obj); 77 | assertSame(queryExecutionInfo, listener.getEachQueryResultExecutionInfo()); 78 | 79 | // verify EACH_QUERY_RESULT 80 | assertEquals(ProxyEventType.EACH_QUERY_RESULT, queryExecutionInfo.getProxyEventType()); 81 | assertEquals(2, queryExecutionInfo.getCurrentResultCount()); 82 | assertEquals("bar", queryExecutionInfo.getCurrentMappedResult()); 83 | assertEquals(threadId, queryExecutionInfo.getThreadId()); 84 | assertEquals(threadName, queryExecutionInfo.getThreadName()); 85 | assertNull(queryExecutionInfo.getThrowable()); 86 | }) 87 | .assertNext(obj -> { // third 88 | assertEquals("baz", obj); 89 | assertSame(queryExecutionInfo, listener.getEachQueryResultExecutionInfo()); 90 | 91 | // verify EACH_QUERY_RESULT 92 | assertEquals(ProxyEventType.EACH_QUERY_RESULT, queryExecutionInfo.getProxyEventType()); 93 | assertEquals(3, queryExecutionInfo.getCurrentResultCount()); 94 | assertEquals("baz", queryExecutionInfo.getCurrentMappedResult()); 95 | assertEquals(threadId, queryExecutionInfo.getThreadId()); 96 | assertEquals(threadName, queryExecutionInfo.getThreadName()); 97 | assertNull(queryExecutionInfo.getThrowable()); 98 | }) 99 | .verifyComplete(); 100 | 101 | } 102 | 103 | @Test 104 | void mapWithPublisherException() throws Throwable { 105 | LastExecutionAwareListener listener = new LastExecutionAwareListener(); 106 | CompositeProxyExecutionListener compositeListener = new CompositeProxyExecutionListener(listener); 107 | 108 | QueryExecutionInfo queryExecutionInfo = new QueryExecutionInfo(); 109 | ProxyConfig proxyConfig = new ProxyConfig(); 110 | proxyConfig.addListener(compositeListener); 111 | 112 | 113 | // return a publisher that throws exception at execution 114 | Exception exception = new RuntimeException("map exception"); 115 | TestPublisher publisher = TestPublisher.create().error(exception); 116 | 117 | Result mockResult = mock(Result.class); 118 | when(mockResult.map(any())).thenReturn(publisher); 119 | 120 | ReactiveResultCallback callback = new ReactiveResultCallback(mockResult, queryExecutionInfo, proxyConfig); 121 | 122 | // since "mockResult.map()" is mocked, args can be anything as long as num of args matches to signature. 123 | Object[] args = new Object[]{null}; 124 | Object result = callback.invoke(null, MAP_METHOD, args); 125 | 126 | assertThat(result).isInstanceOf(Publisher.class); 127 | assertNotSame(publisher, result); 128 | 129 | long threadId = Thread.currentThread().getId(); 130 | String threadName = Thread.currentThread().getName(); 131 | 132 | StepVerifier.create((Publisher) result) 133 | .expectSubscription() 134 | .consumeErrorWith(thrown -> { 135 | assertSame(exception, thrown); 136 | }) 137 | .verify(); 138 | 139 | assertSame(queryExecutionInfo, listener.getEachQueryResultExecutionInfo()); 140 | 141 | // verify EACH_QUERY_RESULT 142 | assertEquals(ProxyEventType.EACH_QUERY_RESULT, queryExecutionInfo.getProxyEventType()); 143 | assertEquals(1, queryExecutionInfo.getCurrentResultCount()); 144 | assertNull(queryExecutionInfo.getCurrentMappedResult()); 145 | assertEquals(threadId, queryExecutionInfo.getThreadId()); 146 | assertEquals(threadName, queryExecutionInfo.getThreadName()); 147 | } 148 | 149 | @Test 150 | void mapWithEmptyPublisher() throws Throwable { 151 | LastExecutionAwareListener listener = new LastExecutionAwareListener(); 152 | CompositeProxyExecutionListener compositeListener = new CompositeProxyExecutionListener(listener); 153 | 154 | QueryExecutionInfo queryExecutionInfo = new QueryExecutionInfo(); 155 | ProxyConfig proxyConfig = new ProxyConfig(); 156 | proxyConfig.addListener(compositeListener); 157 | 158 | // return empty publisher 159 | Publisher publisher = Flux.empty(); 160 | Result mockResult = mock(Result.class); 161 | when(mockResult.map(any())).thenReturn(publisher); 162 | 163 | ReactiveResultCallback callback = new ReactiveResultCallback(mockResult, queryExecutionInfo, proxyConfig); 164 | 165 | // since "mockResult.map()" is mocked, args can be anything as long as num of args matches to signature. 166 | Object[] args = new Object[]{null}; 167 | Object result = callback.invoke(null, MAP_METHOD, args); 168 | 169 | assertThat(result) 170 | .isInstanceOf(Publisher.class) 171 | .isNotSameAs(publisher); 172 | 173 | StepVerifier.create((Publisher) result) 174 | .expectSubscription() 175 | .verifyComplete(); 176 | 177 | assertNull(listener.getEachQueryResultExecutionInfo(), 178 | "EachQueryResult callback should not be called"); 179 | } 180 | 181 | @Test 182 | void mapWithResultThatErrorsAtExecutionTime() throws Throwable { 183 | 184 | // call to the "map()" method returns a publisher that fails(errors) at execution time 185 | 186 | LastExecutionAwareListener listener = new LastExecutionAwareListener(); 187 | CompositeProxyExecutionListener compositeListener = new CompositeProxyExecutionListener(listener); 188 | 189 | QueryExecutionInfo queryExecutionInfo = new QueryExecutionInfo(); 190 | ProxyConfig proxyConfig = new ProxyConfig(); 191 | proxyConfig.addListener(compositeListener); 192 | 193 | 194 | RuntimeException exception = new RuntimeException("failure"); 195 | 196 | // publisher that fails at execution 197 | Publisher source = Flux.just("foo", "bar", "baz") 198 | .map(str -> { 199 | throw exception; 200 | }); 201 | 202 | Result mockResult = mock(Result.class); 203 | when(mockResult.map(any())).thenReturn(source); 204 | 205 | ReactiveResultCallback callback = new ReactiveResultCallback(mockResult, queryExecutionInfo, proxyConfig); 206 | 207 | // since "mockResult.map()" is mocked, args can be anything as long as num of args matches to signature. 208 | Object[] args = new Object[]{null}; 209 | Object result = callback.invoke(null, MAP_METHOD, args); 210 | 211 | assertThat(result) 212 | .isInstanceOf(Publisher.class) 213 | .isNotSameAs(source); 214 | 215 | Flux resultConsumer = Flux.from((Publisher) result); 216 | 217 | long threadId = Thread.currentThread().getId(); 218 | String threadName = Thread.currentThread().getName(); 219 | 220 | StepVerifier.create(resultConsumer) 221 | .expectSubscription() 222 | .consumeErrorWith(thrown -> { 223 | assertSame(exception, thrown); 224 | }) 225 | .verify(); 226 | 227 | // verify callback 228 | assertSame(queryExecutionInfo, listener.getEachQueryResultExecutionInfo(), 229 | "listener should be called even consuming throws exception"); 230 | assertEquals(ProxyEventType.EACH_QUERY_RESULT, queryExecutionInfo.getProxyEventType()); 231 | assertEquals(1, queryExecutionInfo.getCurrentResultCount()); 232 | assertNull(queryExecutionInfo.getCurrentMappedResult()); 233 | assertSame(exception, queryExecutionInfo.getThrowable()); 234 | assertEquals(threadId, queryExecutionInfo.getThreadId()); 235 | assertEquals(threadName, queryExecutionInfo.getThreadName()); 236 | 237 | } 238 | 239 | @Test 240 | void getTarget() throws Throwable { 241 | Result mockResult = mock(Result.class); 242 | QueryExecutionInfo queryExecutionInfo = new QueryExecutionInfo(); 243 | ProxyConfig proxyConfig = new ProxyConfig(); 244 | 245 | ReactiveResultCallback callback = new ReactiveResultCallback(mockResult, queryExecutionInfo, proxyConfig); 246 | 247 | Object result = callback.invoke(null, GET_TARGET_METHOD, null); 248 | assertSame(mockResult, result); 249 | } 250 | 251 | } 252 | -------------------------------------------------------------------------------- /src/test/java/net/ttddyy/dsproxy/r2dbc/proxy/ReactiveStatementCallbackTest.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.proxy; 2 | 3 | import io.r2dbc.spi.Statement; 4 | import net.ttddyy.dsproxy.r2dbc.core.Binding; 5 | import net.ttddyy.dsproxy.r2dbc.core.BindingValue; 6 | import net.ttddyy.dsproxy.r2dbc.core.Bindings; 7 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 8 | import net.ttddyy.dsproxy.r2dbc.support.LastExecutionAwareListener; 9 | import net.ttddyy.dsproxy.r2dbc.core.QueryExecutionInfo; 10 | import net.ttddyy.dsproxy.r2dbc.core.QueryInfo; 11 | import org.junit.jupiter.api.Test; 12 | import org.reactivestreams.Publisher; 13 | import org.springframework.util.ReflectionUtils; 14 | import reactor.core.publisher.Flux; 15 | import reactor.test.StepVerifier; 16 | 17 | import java.lang.reflect.Method; 18 | import java.util.List; 19 | 20 | import static java.util.stream.Collectors.toList; 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | import static org.junit.jupiter.api.Assertions.assertEquals; 23 | import static org.junit.jupiter.api.Assertions.assertNotNull; 24 | import static org.junit.jupiter.api.Assertions.assertSame; 25 | import static org.mockito.Mockito.mock; 26 | import static org.mockito.Mockito.when; 27 | 28 | /** 29 | * @author Tadaya Tsuyukubo 30 | */ 31 | public class ReactiveStatementCallbackTest { 32 | 33 | private static Method ADD_METHOD = ReflectionUtils.findMethod(Statement.class, "add"); 34 | private static Method EXECUTE_METHOD = ReflectionUtils.findMethod(Statement.class, "execute"); 35 | private static Method BIND_BY_INDEX_METHOD = ReflectionUtils.findMethod(Statement.class, "bind", int.class, Object.class); 36 | private static Method BIND_BY_ID_METHOD = ReflectionUtils.findMethod(Statement.class, "bind", Object.class, Object.class); 37 | private static Method BIND_NULL_BY_INDEX_METHOD = ReflectionUtils.findMethod(Statement.class, "bindNull", int.class, Class.class); 38 | private static Method BIND_NULL_BY_ID_METHOD = ReflectionUtils.findMethod(Statement.class, "bindNull", Object.class, Class.class); 39 | private static Method GET_TARGET_METHOD = ReflectionUtils.findMethod(ProxyObject.class, "getTarget"); 40 | 41 | @Test 42 | void add() throws Throwable { 43 | LastExecutionAwareListener testListener = new LastExecutionAwareListener(); 44 | 45 | ConnectionInfo connectionInfo = new ConnectionInfo(); 46 | ProxyConfig proxyConfig = new ProxyConfig(); 47 | proxyConfig.addListener(testListener); 48 | Statement statement = mock(Statement.class); 49 | Statement mockResult = mock(Statement.class); 50 | 51 | when(statement.add()).thenReturn(mockResult); 52 | 53 | ReactiveStatementCallback callback = new ReactiveStatementCallback(statement, null, connectionInfo, proxyConfig); 54 | 55 | Object result = callback.invoke(null, ADD_METHOD, null); 56 | 57 | assertSame(mockResult, result); 58 | } 59 | 60 | @Test 61 | void executeOperationWithBindByIndex() throws Throwable { 62 | LastExecutionAwareListener testListener = new LastExecutionAwareListener(); 63 | 64 | String query = "QUERY"; 65 | ConnectionInfo connectionInfo = new ConnectionInfo(); 66 | ProxyConfig proxyConfig = new ProxyConfig(); 67 | proxyConfig.addListener(testListener); 68 | Statement statement = mock(Statement.class); 69 | ReactiveStatementCallback callback = new ReactiveStatementCallback(statement, query, connectionInfo, proxyConfig); 70 | 71 | when(statement.execute()).thenReturn(Flux.empty()); 72 | 73 | callback.invoke(null, BIND_BY_INDEX_METHOD, new Object[]{1, 100}); 74 | callback.invoke(null, BIND_NULL_BY_INDEX_METHOD, new Object[]{2, String.class}); 75 | callback.invoke(null, ADD_METHOD, null); 76 | callback.invoke(null, BIND_NULL_BY_INDEX_METHOD, new Object[]{1, int.class}); 77 | callback.invoke(null, BIND_BY_INDEX_METHOD, new Object[]{2, 200}); 78 | Object result = callback.invoke(null, EXECUTE_METHOD, null); 79 | 80 | 81 | StepVerifier.create((Publisher) result) 82 | .verifyComplete(); 83 | 84 | QueryExecutionInfo afterQueryInfo = testListener.getAfterQueryExecutionInfo(); 85 | 86 | assertNotNull(afterQueryInfo); 87 | 88 | assertEquals(0, afterQueryInfo.getBatchSize()); 89 | assertEquals(2, afterQueryInfo.getBindingsSize()); 90 | assertThat(afterQueryInfo.getQueries()) 91 | .hasSize(1) 92 | .extracting(QueryInfo::getQuery) 93 | .containsExactly(query); 94 | QueryInfo queryInfo = afterQueryInfo.getQueries().get(0); 95 | 96 | assertThat(queryInfo.getBindingsList()).hasSize(2); 97 | Bindings firstBindings = queryInfo.getBindingsList().get(0); 98 | Bindings secondBindings = queryInfo.getBindingsList().get(1); 99 | 100 | 101 | assertThat(firstBindings.getIndexBindings()) 102 | .hasSize(2) 103 | .extracting(Binding::getKey) 104 | .containsExactly(1, 2); 105 | assertThat(firstBindings.getIdentifierBindings()).isEmpty(); 106 | 107 | List bindingValues = firstBindings.getIndexBindings().stream() 108 | .map(Binding::getBindingValue) 109 | .collect(toList()); 110 | 111 | // for "bind(1, 100)" 112 | assertThat(bindingValues.get(0)) 113 | .isExactlyInstanceOf(BindingValue.SimpleBindingValue.class) 114 | .extracting(BindingValue::getValue) 115 | .isEqualTo(100); 116 | 117 | // for "bindNull(2, String.class)" 118 | assertThat(bindingValues.get(1)) 119 | .isExactlyInstanceOf(BindingValue.NullBindingValue.class); 120 | BindingValue.NullBindingValue nullBindValue = (BindingValue.NullBindingValue) bindingValues.get(1); 121 | assertThat(nullBindValue.getType()).isEqualTo(String.class); 122 | 123 | 124 | assertThat(secondBindings.getIndexBindings()) 125 | .hasSize(2) 126 | .extracting(Binding::getKey) 127 | .containsExactly(1, 2); 128 | assertThat(secondBindings.getIdentifierBindings()).isEmpty(); 129 | 130 | bindingValues = secondBindings.getIndexBindings().stream() 131 | .map(Binding::getBindingValue) 132 | .collect(toList()); 133 | 134 | // for "bindNull(1, int.class)" 135 | assertThat(bindingValues.get(0)) 136 | .isExactlyInstanceOf(BindingValue.NullBindingValue.class); 137 | nullBindValue = (BindingValue.NullBindingValue) bindingValues.get(0); 138 | assertThat(nullBindValue.getType()).isEqualTo(int.class); 139 | 140 | // for "bind(2, 200)" 141 | assertThat(bindingValues.get(1)) 142 | .isExactlyInstanceOf(BindingValue.SimpleBindingValue.class) 143 | .extracting(BindingValue::getValue) 144 | .isEqualTo(200); 145 | 146 | } 147 | 148 | @Test 149 | void executeOperationWithBindByIdentifier() throws Throwable { 150 | LastExecutionAwareListener testListener = new LastExecutionAwareListener(); 151 | 152 | String query = "QUERY"; 153 | ConnectionInfo connectionInfo = new ConnectionInfo(); 154 | ProxyConfig proxyConfig = new ProxyConfig(); 155 | proxyConfig.addListener(testListener); 156 | Statement statement = mock(Statement.class); 157 | ReactiveStatementCallback callback = new ReactiveStatementCallback(statement, query, connectionInfo, proxyConfig); 158 | 159 | when(statement.execute()).thenReturn(Flux.empty()); 160 | 161 | callback.invoke(null, BIND_BY_ID_METHOD, new Object[]{"$1", 100}); 162 | callback.invoke(null, BIND_NULL_BY_ID_METHOD, new Object[]{"$2", String.class}); 163 | callback.invoke(null, ADD_METHOD, null); 164 | callback.invoke(null, BIND_NULL_BY_ID_METHOD, new Object[]{"$1", int.class}); 165 | callback.invoke(null, BIND_BY_ID_METHOD, new Object[]{"$2", 200}); 166 | Object result = callback.invoke(null, EXECUTE_METHOD, null); 167 | 168 | 169 | StepVerifier.create((Publisher) result) 170 | .verifyComplete(); 171 | 172 | QueryExecutionInfo afterQueryInfo = testListener.getAfterQueryExecutionInfo(); 173 | 174 | assertNotNull(afterQueryInfo); 175 | 176 | assertEquals(0, afterQueryInfo.getBatchSize()); 177 | assertEquals(2, afterQueryInfo.getBindingsSize()); 178 | assertThat(afterQueryInfo.getQueries()) 179 | .hasSize(1) 180 | .extracting(QueryInfo::getQuery) 181 | .containsExactly(query); 182 | QueryInfo queryInfo = afterQueryInfo.getQueries().get(0); 183 | 184 | assertThat(queryInfo.getBindingsList()).hasSize(2); 185 | Bindings firstBindings = queryInfo.getBindingsList().get(0); 186 | Bindings secondBindings = queryInfo.getBindingsList().get(1); 187 | 188 | 189 | assertThat(firstBindings.getIndexBindings()).isEmpty(); 190 | assertThat(firstBindings.getIdentifierBindings()) 191 | .hasSize(2) 192 | .extracting(Binding::getKey) 193 | .containsExactly("$1", "$2"); 194 | 195 | List bindingValues = firstBindings.getIdentifierBindings().stream() 196 | .map(Binding::getBindingValue) 197 | .collect(toList()); 198 | 199 | // for "bind(1, 100)" 200 | assertThat(bindingValues.get(0)) 201 | .isExactlyInstanceOf(BindingValue.SimpleBindingValue.class) 202 | .extracting(BindingValue::getValue) 203 | .isEqualTo(100); 204 | 205 | // for "bindNull(2, String.class)" 206 | assertThat(bindingValues.get(1)) 207 | .isExactlyInstanceOf(BindingValue.NullBindingValue.class); 208 | BindingValue.NullBindingValue nullBindValue = (BindingValue.NullBindingValue) bindingValues.get(1); 209 | assertThat(nullBindValue.getType()).isEqualTo(String.class); 210 | 211 | 212 | assertThat(secondBindings.getIndexBindings()).isEmpty(); 213 | assertThat(secondBindings.getIdentifierBindings()) 214 | .hasSize(2) 215 | .extracting(Binding::getKey) 216 | .containsExactly("$1", "$2"); 217 | 218 | bindingValues = secondBindings.getIdentifierBindings().stream() 219 | .map(Binding::getBindingValue) 220 | .collect(toList()); 221 | 222 | // for "bindNull(1, int.class)" 223 | assertThat(bindingValues.get(0)) 224 | .isExactlyInstanceOf(BindingValue.NullBindingValue.class); 225 | nullBindValue = (BindingValue.NullBindingValue) bindingValues.get(0); 226 | assertThat(nullBindValue.getType()).isEqualTo(int.class); 227 | 228 | // for "bind(2, 200)" 229 | assertThat(bindingValues.get(1)) 230 | .isExactlyInstanceOf(BindingValue.SimpleBindingValue.class) 231 | .extracting(BindingValue::getValue) 232 | .isEqualTo(200); 233 | 234 | } 235 | 236 | @Test 237 | void getTarget() throws Throwable { 238 | Statement statement = mock(Statement.class); 239 | ConnectionInfo connectionInfo = new ConnectionInfo(); 240 | ProxyConfig proxyConfig = new ProxyConfig(); 241 | String query = "QUERY"; 242 | 243 | ReactiveStatementCallback callback = new ReactiveStatementCallback(statement, query, connectionInfo, proxyConfig); 244 | 245 | Object result = callback.invoke(null, GET_TARGET_METHOD, null); 246 | assertSame(statement, result); 247 | } 248 | 249 | } 250 | -------------------------------------------------------------------------------- /src/test/java/net/ttddyy/dsproxy/r2dbc/support/LifeCycleExecutionListenerTest.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.support; 2 | 3 | import net.ttddyy.dsproxy.r2dbc.core.ExecutionType; 4 | import net.ttddyy.dsproxy.r2dbc.core.MethodExecutionInfo; 5 | import net.ttddyy.dsproxy.r2dbc.core.QueryExecutionInfo; 6 | import org.aopalliance.intercept.MethodInterceptor; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.springframework.aop.framework.ProxyFactory; 10 | import org.springframework.util.StringUtils; 11 | 12 | import java.lang.reflect.Method; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.mockito.Mockito.mock; 19 | import static org.mockito.Mockito.reset; 20 | import static org.mockito.Mockito.when; 21 | 22 | /** 23 | * @author Tadaya Tsuyukubo 24 | */ 25 | public class LifeCycleExecutionListenerTest { 26 | 27 | /** 28 | * Test invoking {@link LifeCycleExecutionListener#beforeMethod(MethodExecutionInfo)} and 29 | * {@link LifeCycleExecutionListener#afterMethod(MethodExecutionInfo)} invokes corresponding 30 | * before/after methods defined on {@link LifeCycleListener}. 31 | * 32 | * @param clazz class that datasource-proxy-r2dbc creates proxy 33 | */ 34 | @ParameterizedTest 35 | @ProxyClassesSource 36 | void methodInvocations(Class clazz) { 37 | String className = clazz.getSimpleName(); 38 | 39 | List invokedMethods = new ArrayList<>(); 40 | LifeCycleListener lifeCycleListener = createLifeCycleListener(invokedMethods); 41 | LifeCycleExecutionListener listener = new LifeCycleExecutionListener(lifeCycleListener); 42 | 43 | MethodExecutionInfo methodExecutionInfo = mock(MethodExecutionInfo.class); 44 | 45 | Method[] declaredMethods = clazz.getDeclaredMethods(); 46 | for (Method methodToInvoke : declaredMethods) { 47 | String methodName = methodToInvoke.getName(); 48 | 49 | // beforeXxxOnYyy : Xxx is a capitalized method-name and Yyy is a capitalized class-name 50 | String expectedBeforeMethodName = "before" + StringUtils.capitalize(methodName) + "On" + StringUtils.capitalize(className); 51 | String expectedAfterMethodName = "after" + StringUtils.capitalize(methodName) + "On" + StringUtils.capitalize(className); 52 | 53 | // mock executing method 54 | when(methodExecutionInfo.getMethod()).thenReturn(methodToInvoke); 55 | 56 | // invoke beforeMethod() 57 | listener.beforeMethod(methodExecutionInfo); 58 | 59 | // first method is beforeMethod 60 | // second method is beforeXxxOnYyy 61 | assertThat(invokedMethods) 62 | .hasSize(2) 63 | .extracting(Method::getName) 64 | .containsExactly("beforeMethod", expectedBeforeMethodName) 65 | ; 66 | 67 | // extra check for beforeXxxOnYyy 68 | Method beforeXxxOnYyy = invokedMethods.get(1); 69 | assertEquals(LifeCycleListener.class, beforeXxxOnYyy.getDeclaringClass()); 70 | 71 | // reset 72 | invokedMethods.clear(); 73 | 74 | listener.afterMethod(methodExecutionInfo); 75 | 76 | // first method is afterXxxOnYyy 77 | // second method is afterMethod 78 | assertThat(invokedMethods) 79 | .hasSize(2) 80 | .extracting(Method::getName) 81 | .containsExactly(expectedAfterMethodName, "afterMethod") 82 | ; 83 | 84 | // extra check for afterXxxOnYyy 85 | Method afterXxxOnYyy = invokedMethods.get(0); 86 | assertEquals(LifeCycleListener.class, afterXxxOnYyy.getDeclaringClass()); 87 | 88 | // reset 89 | invokedMethods.clear(); 90 | reset(methodExecutionInfo); 91 | } 92 | 93 | } 94 | 95 | @Test 96 | void queryExecution() { 97 | 98 | List invokedMethods = new ArrayList<>(); 99 | LifeCycleListener lifeCycleListener = createLifeCycleListener(invokedMethods); 100 | LifeCycleExecutionListener listener = new LifeCycleExecutionListener(lifeCycleListener); 101 | 102 | QueryExecutionInfo queryExecutionInfo; 103 | 104 | // for Statement#execute 105 | queryExecutionInfo = new QueryExecutionInfo(); 106 | queryExecutionInfo.setType(ExecutionType.STATEMENT); 107 | 108 | // test beforeQuery 109 | listener.beforeQuery(queryExecutionInfo); 110 | verifyQueryExecutionInvocation(invokedMethods, "beforeQuery", "beforeExecuteOnStatement"); 111 | 112 | invokedMethods.clear(); 113 | 114 | // test afterQuery 115 | listener.afterQuery(queryExecutionInfo); 116 | verifyQueryExecutionInvocation(invokedMethods, "afterExecuteOnStatement", "afterQuery"); 117 | 118 | assertEquals(LifeCycleListener.class, invokedMethods.get(0).getDeclaringClass()); 119 | assertEquals(LifeCycleListener.class, invokedMethods.get(1).getDeclaringClass()); 120 | 121 | invokedMethods.clear(); 122 | 123 | 124 | // for Batch#execute 125 | queryExecutionInfo = new QueryExecutionInfo(); 126 | queryExecutionInfo.setType(ExecutionType.BATCH); 127 | 128 | // test beforeQuery 129 | listener.beforeQuery(queryExecutionInfo); 130 | verifyQueryExecutionInvocation(invokedMethods, "beforeQuery", "beforeExecuteOnBatch"); 131 | invokedMethods.clear(); 132 | 133 | // test afterQuery 134 | listener.afterQuery(queryExecutionInfo); 135 | verifyQueryExecutionInvocation(invokedMethods, "afterExecuteOnBatch", "afterQuery"); 136 | 137 | } 138 | 139 | private void verifyQueryExecutionInvocation(List invokedMethods, String... expectedMethodNames) { 140 | assertThat(invokedMethods) 141 | .hasSize(2) 142 | .extracting(Method::getName) 143 | .containsExactly(expectedMethodNames) 144 | ; 145 | 146 | assertEquals(LifeCycleListener.class, invokedMethods.get(0).getDeclaringClass()); 147 | assertEquals(LifeCycleListener.class, invokedMethods.get(1).getDeclaringClass()); 148 | 149 | } 150 | 151 | private LifeCycleListener createLifeCycleListener(List invokedMethods) { 152 | // use spring aop framework to create a proxy of LifeCycleListener that just keeps the 153 | // invoked methods 154 | MethodInterceptor interceptor = invocation -> { 155 | invokedMethods.add(invocation.getMethod()); 156 | return null; // don't proceed the proxy 157 | }; 158 | 159 | ProxyFactory proxyFactory = new ProxyFactory(); 160 | proxyFactory.addAdvice(interceptor); 161 | proxyFactory.addInterface(LifeCycleListener.class); 162 | return (LifeCycleListener) proxyFactory.getProxy(); 163 | } 164 | 165 | 166 | // TODO: add test for onEachQueryResult 167 | 168 | 169 | } 170 | -------------------------------------------------------------------------------- /src/test/java/net/ttddyy/dsproxy/r2dbc/support/LifeCycleListenerTest.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.support; 2 | 3 | import org.junit.jupiter.params.ParameterizedTest; 4 | import org.springframework.util.StringUtils; 5 | 6 | import java.lang.reflect.Method; 7 | import java.util.Set; 8 | import java.util.stream.Stream; 9 | 10 | import static java.util.stream.Collectors.toSet; 11 | import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; 12 | 13 | /** 14 | * @author Tadaya Tsuyukubo 15 | */ 16 | public class LifeCycleListenerTest { 17 | 18 | @ParameterizedTest 19 | @ProxyClassesSource 20 | void verifyMethodNames(Class clazz) { 21 | 22 | String className = clazz.getSimpleName(); 23 | 24 | Set expected = Stream.of(clazz.getDeclaredMethods()) 25 | .map(Method::getName) 26 | .flatMap(methodName -> { 27 | // beforeXxxOnYyy / afterXxxOnYyy 28 | String name = StringUtils.capitalize(methodName) + "On" + StringUtils.capitalize(className); 29 | return Stream.of("before" + name, "after" + name); 30 | }) 31 | .collect(toSet()); 32 | 33 | Set actual = Stream.of(LifeCycleListener.class.getDeclaredMethods()) 34 | .map(Method::getName) 35 | .collect(toSet()); 36 | 37 | assertThat(actual).containsAll(expected); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/net/ttddyy/dsproxy/r2dbc/support/MethodExecutionInfoFormatterTest.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.support; 2 | 3 | import io.r2dbc.spi.ConnectionFactory; 4 | import net.ttddyy.dsproxy.r2dbc.core.ConnectionInfo; 5 | import net.ttddyy.dsproxy.r2dbc.core.MethodExecutionInfo; 6 | import net.ttddyy.dsproxy.r2dbc.support.MethodExecutionInfoFormatter; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.util.ReflectionUtils; 9 | 10 | import java.lang.reflect.Method; 11 | import java.time.Duration; 12 | import java.time.temporal.ChronoUnit; 13 | 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | 16 | /** 17 | * @author Tadaya Tsuyukubo 18 | */ 19 | public class MethodExecutionInfoFormatterTest { 20 | 21 | @Test 22 | void withDefault() { 23 | 24 | // String#indexOf(int) method 25 | Method method = ReflectionUtils.findMethod(String.class, "indexOf", int.class); 26 | 27 | Long target = 100L; 28 | 29 | ConnectionInfo connectionInfo = new ConnectionInfo(); 30 | connectionInfo.setConnectionId("ABC"); 31 | 32 | MethodExecutionInfo executionInfo = new MethodExecutionInfo(); 33 | executionInfo.setThreadId(5); 34 | executionInfo.setConnectionInfo(connectionInfo); 35 | executionInfo.setExecuteDuration(Duration.of(23, ChronoUnit.MILLIS)); 36 | executionInfo.setMethod(method); 37 | executionInfo.setTarget(target); 38 | 39 | MethodExecutionInfoFormatter formatter = MethodExecutionInfoFormatter.withDefault(); 40 | String result = formatter.format(executionInfo); 41 | 42 | assertEquals(" 1: Thread:5 Connection:ABC Time:23 Long#indexOf()", result); 43 | 44 | // second time should increase the sequence 45 | result = formatter.format(executionInfo); 46 | assertEquals(" 2: Thread:5 Connection:ABC Time:23 Long#indexOf()", result); 47 | 48 | } 49 | 50 | @Test 51 | void nullConnectionId() { 52 | 53 | // connection id is null for before execution of "ConnectionFactory#create" 54 | Method method = ReflectionUtils.findMethod(ConnectionFactory.class, "create"); 55 | 56 | Long target = 100L; 57 | 58 | // null ConnectionInfo 59 | MethodExecutionInfo executionInfo = new MethodExecutionInfo(); 60 | executionInfo.setThreadId(5); 61 | executionInfo.setConnectionInfo(null); 62 | executionInfo.setExecuteDuration(Duration.of(23, ChronoUnit.MILLIS)); 63 | executionInfo.setMethod(method); 64 | executionInfo.setTarget(target); 65 | 66 | MethodExecutionInfoFormatter formatter = MethodExecutionInfoFormatter.withDefault(); 67 | String result = formatter.format(executionInfo); 68 | 69 | assertEquals(" 1: Thread:5 Connection:n/a Time:23 Long#create()", result); 70 | 71 | // null ConnectionId 72 | executionInfo.setConnectionInfo(new ConnectionInfo()); 73 | result = formatter.format(executionInfo); 74 | 75 | assertEquals(" 2: Thread:5 Connection:n/a Time:23 Long#create()", result); 76 | } 77 | 78 | @Test 79 | void customConsumer() { 80 | 81 | MethodExecutionInfoFormatter formatter = new MethodExecutionInfoFormatter(); 82 | formatter.addConsumer((executionInfo, sb) -> { 83 | sb.append("ABC"); 84 | }); 85 | String result = formatter.format(new MethodExecutionInfo()); 86 | 87 | assertEquals("ABC", result); 88 | 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/net/ttddyy/dsproxy/r2dbc/support/ProxyClassesSource.java: -------------------------------------------------------------------------------- 1 | package net.ttddyy.dsproxy.r2dbc.support; 2 | 3 | import io.r2dbc.spi.Batch; 4 | import io.r2dbc.spi.Connection; 5 | import io.r2dbc.spi.ConnectionFactory; 6 | import io.r2dbc.spi.Result; 7 | import io.r2dbc.spi.Statement; 8 | import org.junit.jupiter.params.provider.ValueSource; 9 | 10 | import java.lang.annotation.Retention; 11 | import java.lang.annotation.RetentionPolicy; 12 | 13 | /** 14 | * Provides classes that datasource-proxy-r2dbc generates proxies. 15 | * 16 | * Composed annotation for {@link org.junit.jupiter.params.ParameterizedTest}. 17 | * 18 | * @author Tadaya Tsuyukubo 19 | */ 20 | @Retention(RetentionPolicy.RUNTIME) 21 | @ValueSource(classes = {ConnectionFactory.class, Connection.class, Batch.class, Statement.class, Result.class}) 22 | public @interface ProxyClassesSource { 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %date{HH:mm:ss.SSS} %-18thread %-55logger %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | --------------------------------------------------------------------------------