diff --git a/settings.gradle.kts b/settings.gradle.kts index 5842bee..2f4f2a2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,6 +18,7 @@ include("ts-core:ts-yaml") include("ts-core:ts-json") include("ts-core:ts-xml") include("ts-core:ts-async-http") +include("ts-core:ts-proxy") include("ts-socket") include("ts-web") include("ts-web:ts-web-netty") diff --git a/ts-core/ts-proxy/build.gradle.kts b/ts-core/ts-proxy/build.gradle.kts new file mode 100644 index 0000000..4a7df74 --- /dev/null +++ b/ts-core/ts-proxy/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + kotlin("jvm") + `maven-publish` + id("ts-gradle") + kotlin("plugin.allopen") version "1.5.21" +} + +dependencies { + api(project(":ts-core")) + implementation("cglib:cglib:3.3.0") + implementation("org.apache.commons", "commons-lang3", "3.8.1") + testApi(group = "junit", name = "junit", version = "4.13.2") +} + +artifacts { + archives(tasks["kotlinSourcesJar"]) +} diff --git a/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/ListProxy.kt b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/ListProxy.kt new file mode 100644 index 0000000..850730b --- /dev/null +++ b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/ListProxy.kt @@ -0,0 +1,27 @@ +package cn.tursom.proxy + + +class ListProxy : MutableProxy { + private val proxyList: MutableList = ArrayList() + + override fun addProxy(proxy: T): Int { + proxyList.add(proxy) + return proxyList.size - 1 + } + + override fun addAllProxy(proxy: Collection?): Boolean { + return proxyList.addAll(proxy!!) + } + + override fun removeProxy(proxy: T) { + proxyList.remove(proxy) + } + + override fun removeProxy(index: Int) { + proxyList.removeAt(index) + } + + override fun iterator(): MutableIterator { + return proxyList.iterator() + } +} \ No newline at end of file diff --git a/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/MutableProxy.kt b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/MutableProxy.kt new file mode 100644 index 0000000..7bc09a7 --- /dev/null +++ b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/MutableProxy.kt @@ -0,0 +1,8 @@ +package cn.tursom.proxy + +interface MutableProxy : Proxy { + fun addProxy(proxy: T): Int + fun addAllProxy(proxy: Collection?): Boolean + fun removeProxy(proxy: T) + fun removeProxy(index: Int) +} \ No newline at end of file diff --git a/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/Proxy.kt b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/Proxy.kt new file mode 100644 index 0000000..bbb7870 --- /dev/null +++ b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/Proxy.kt @@ -0,0 +1,46 @@ +package cn.tursom.proxy + +import cn.tursom.core.uncheckedCast + +interface Proxy : Iterable { + data class Result( + val result: R, + val success: Boolean = false, + ) + + companion object { + val failed: Result<*> = Result(null, false) + fun of(): Result { + return of(null) + } + + /** + * 返回一个临时使用的 Result 对象 + * 因为是临时对象,所以不要把这个对象放到任何当前函数堆栈以外的地方 + * 如果要长期储存对象请 new Result + */ + fun of(result: R): Result { + return Result(result, true) + } + + fun failed(): Result { + return failed.uncheckedCast() + } + } +} + +inline fun Proxy.forEachProxy(action: (T) -> Unit) { + for (t in this) { + action(t) + } +} + +inline fun Proxy.forFirstProxy(action: (T) -> Proxy.Result?): Proxy.Result { + for (t in this) { + val result = action(t) + if (result != null && result.success) { + return result + } + } + return Proxy.failed() +} \ No newline at end of file diff --git a/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/ProxyContainer.kt b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/ProxyContainer.kt new file mode 100644 index 0000000..a1927f5 --- /dev/null +++ b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/ProxyContainer.kt @@ -0,0 +1,115 @@ +package cn.tursom.proxy + +import cn.tursom.proxy.annotation.ForEachProxy +import cn.tursom.proxy.annotation.ForFirstProxy +import net.sf.cglib.proxy.MethodProxy +import org.apache.commons.lang3.StringUtils +import java.lang.reflect.Method +import java.util.* + +interface ProxyContainer { + @get:Throws(Throwable::class) + val proxy: Proxy + + /** + * will be call when proxy method invoke. + * 在代理方法被调用时,该方法会被调用 + */ + @Throws(Throwable::class) + fun onProxy(method: Method, args: Array, proxy: MethodProxy): Proxy.Result<*>? { + var handler = ProxyContainerHandlerCache.getHandler(method) + if (handler != null) { + return handler(this, method, args, proxy) + } + for (annotation in method.annotations) when (annotation) { + is ForEachProxy -> { + handler = onForeachProxy(annotation) + break + } + is ForFirstProxy -> { + handler = onForFirstProxy(annotation) + break + } + } + if (handler == null) { + handler = ProxyContainerHandlerCache.callSuper + } + ProxyContainerHandlerCache.setHandler(method, handler) + return handler(this, method, args, proxy) + } + + companion object { + private val errMsgSearchList = arrayOf("%M", "%B", "%A") + + fun onForFirstProxy(forFirstProxy: ForFirstProxy) = { o: Any, m: Method, a: Array, p: MethodProxy -> + onForFirstProxy(o, m, a, p, forFirstProxy, when (forFirstProxy.value.size) { + 0 -> emptyList() + 1 -> listOf(forFirstProxy.value[0].java) + else -> forFirstProxy.value.asSequence().map { it.java }.toSet() + }) + } + + private fun onForFirstProxy( + obj: Any, + method: Method, + args: Array, + proxy: MethodProxy?, + forFirstProxy: ForFirstProxy, + classes: Collection>, + ): Proxy.Result<*> { + if (obj !is ProxyContainer<*>) return Proxy.failed() + val result = obj.proxy.forFirstProxy { p -> + if (classes.isEmpty() || classes.stream().anyMatch { c: Class<*> -> c.isInstance(p) }) { + return@forFirstProxy p.onProxy(obj, method, args, proxy) + } else { + return@forFirstProxy Proxy.failed() + } + } + if (result.success) { + return result + } + + // when request not handled + if (forFirstProxy.must) { + // generate error message + var errMsg: String = forFirstProxy.errMsg + if (errMsg.isBlank()) { + errMsg = "no proxy handled on method %M" + } + val replacementList = arrayOfNulls(errMsgSearchList.size) + // todo use efficient contains + if (errMsg.contains(errMsgSearchList[0])) { + replacementList[0] = method.toString() + } + if (errMsg.contains(errMsgSearchList[1])) { + replacementList[1] = obj.toString() + } + if (errMsg.contains(errMsgSearchList[2])) { + replacementList[2] = Arrays.toString(args) + } + + errMsg = StringUtils.replaceEach(errMsg, errMsgSearchList, replacementList) + throw forFirstProxy.errClass.java.getConstructor(String::class.java).newInstance(errMsg) + } + return Proxy.failed + } + + private fun onForeachProxy(forEachProxy: ForEachProxy) = onForeachProxy(when (forEachProxy.value.size) { + 0 -> emptyList() + 1 -> listOf(forEachProxy.value[0].java) + else -> forEachProxy.value.asSequence().map { it.java }.toSet() + }) + + private fun onForeachProxy( + classes: Collection>, + ) = label@{ o: Any, m: Method, a: Array, proxy1: MethodProxy -> + if (o !is ProxyContainer<*>) return@label Proxy.failed + o.proxy.forEachProxy { p -> + if (classes.isEmpty() || classes.any { c: Class<*> -> c.isInstance(p) }) { + p.onProxy(o, m, a, proxy1) + } + } + Proxy.failed() + } + } +} \ No newline at end of file diff --git a/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/ProxyContainerHandlerCache.kt b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/ProxyContainerHandlerCache.kt new file mode 100644 index 0000000..82e0d2d --- /dev/null +++ b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/ProxyContainerHandlerCache.kt @@ -0,0 +1,22 @@ +package cn.tursom.proxy + +import net.sf.cglib.proxy.MethodProxy +import java.lang.reflect.Method +import java.util.concurrent.ConcurrentHashMap + +object ProxyContainerHandlerCache { + private val handlerMap: MutableMap, MethodProxy) -> Proxy.Result> = + ConcurrentHashMap() + val callSuper = { obj: Any, method: Method, args: Array, proxy: MethodProxy -> + Proxy.of(proxy.invokeSuper(obj, args)) + } + val empty = { obj: Any, method: Method, args: Array, proxy: MethodProxy -> Proxy.failed() } + + fun getHandler(method: Method): ((Any, Method, Array, MethodProxy) -> Proxy.Result)? { + return handlerMap[method] + } + + fun setHandler(method: Method, onProxy: ((Any, Method, Array, MethodProxy) -> Proxy.Result)?) { + handlerMap[method] = onProxy ?: callSuper + } +} \ No newline at end of file diff --git a/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/ProxyInterceptor.kt b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/ProxyInterceptor.kt new file mode 100644 index 0000000..c2d9d50 --- /dev/null +++ b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/ProxyInterceptor.kt @@ -0,0 +1,69 @@ +package cn.tursom.proxy + +import net.sf.cglib.proxy.MethodInterceptor +import net.sf.cglib.proxy.MethodProxy +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.util.* + +class ProxyInterceptor : MethodInterceptor { + companion object { + private val HANDLE_DEQUE_THREAD_LOCAL = ThreadLocal>() + private val parameterTypes = arrayOf(Method::class.java, Array::class.java, MethodProxy::class.java) + private val parameterTypesField: Field = Method::class.java.getDeclaredField("parameterTypes").apply { + isAccessible = true + } + + @Suppress("UNCHECKED_CAST") + private fun getParameterTypes(method: Method): Array> { + return parameterTypesField[method] as Array> + } + + fun equalsMethod(method: Method, name: String?, parameterTypes: Array>?): Boolean { + return method.name == name && parameterTypes.contentEquals(getParameterTypes(method)) + } + + private fun isOnProxyMethod(method: Method): Boolean { + return equalsMethod(method, "onProxy", parameterTypes) + } + + private val handleDeque: ArrayDeque + get() { + var objectArrayDeque = HANDLE_DEQUE_THREAD_LOCAL.get() + if (objectArrayDeque == null) { + objectArrayDeque = ArrayDeque() + HANDLE_DEQUE_THREAD_LOCAL.set(objectArrayDeque) + } + return objectArrayDeque + } + + @Suppress("UNCHECKED_CAST") + fun getHandle(): T { + return handleDeque.first as T + } + + private fun push(obj: Any) { + handleDeque.push(obj) + } + + private fun pop() { + handleDeque.pop() + } + } + + @Throws(Throwable::class) + override fun intercept(obj: Any, method: Method, args: Array, proxy: MethodProxy): Any? { + push(obj) + return try { + if (obj is ProxyContainer<*> && !isOnProxyMethod(method)) { + val result = obj.onProxy(method, args, proxy) + if (result != null && result.success) { + return result.result + } + } + proxy.invokeSuper(obj, args) + } finally { + pop() + } + } +} \ No newline at end of file diff --git a/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/ProxyMethod.kt b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/ProxyMethod.kt new file mode 100644 index 0000000..fa43c84 --- /dev/null +++ b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/ProxyMethod.kt @@ -0,0 +1,71 @@ +package cn.tursom.proxy + +import cn.tursom.proxy.annotation.ForEachProxy +import cn.tursom.proxy.annotation.ForFirstProxy +import net.sf.cglib.proxy.MethodProxy +import java.lang.reflect.Method +import java.util.concurrent.ConcurrentHashMap + +interface ProxyMethod { + @Throws(Throwable::class) + fun onProxy(obj: Any?, method: Method, args: Array, proxy: MethodProxy?): Proxy.Result<*>? { + val selfMethod: Method + val handlerCacheMap = getHandlerCacheMap(javaClass) + val methodResult = handlerCacheMap[method] + if (methodResult != null) { + return if (methodResult.success) { + Proxy.of(methodResult.result(this, *args)) + } else { + Proxy.failed() + } + } + try { + var methodName = method.name + for (annotation in method.annotations) { + if (annotation is ForEachProxy) { + if (annotation.name.isNotEmpty()) { + methodName = annotation.name + break + } + } else if (annotation is ForFirstProxy) { + if (annotation.name.isNotEmpty()) { + methodName = annotation.name + break + } + } + } + selfMethod = javaClass.getMethod(methodName, *method.parameterTypes) + selfMethod.isAccessible = true + handlerCacheMap[method] = Proxy.Result(selfMethod, true) + } catch (e: Exception) { + handlerCacheMap[method] = Proxy.failed() + return Proxy.failed() + } + return Proxy.of(selfMethod(this, *args)) + } + + companion object { + private val handlerCacheMapMap: MutableMap, MutableMap>> = + HashMap() + + fun getHandlerCacheMap(type: Class): MutableMap> { + var handlerCacheMap = handlerCacheMapMap[type] + if (handlerCacheMap == null) synchronized(handlerCacheMapMap) { + handlerCacheMap = handlerCacheMapMap[type] + if (handlerCacheMap == null) { + handlerCacheMap = ConcurrentHashMap() + handlerCacheMapMap[type] = handlerCacheMap!! + } + } + return handlerCacheMap!! + } + + fun getProxyMethod(clazz: Class<*>, name: String, vararg parameterTypes: Class<*>): Method? { + return try { + clazz.getDeclaredMethod(name, *parameterTypes) + } catch (e: NoSuchMethodException) { + throw RuntimeException(e) + } + } + } +} \ No newline at end of file diff --git a/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/annotation/ForEachProxy.kt b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/annotation/ForEachProxy.kt new file mode 100644 index 0000000..0546220 --- /dev/null +++ b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/annotation/ForEachProxy.kt @@ -0,0 +1,7 @@ +package cn.tursom.proxy.annotation + +import kotlin.reflect.KClass + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +@Retention(AnnotationRetention.RUNTIME) +annotation class ForEachProxy(vararg val value: KClass<*> = [], val name: String = "") \ No newline at end of file diff --git a/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/annotation/ForFirstProxy.kt b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/annotation/ForFirstProxy.kt new file mode 100644 index 0000000..65df412 --- /dev/null +++ b/ts-core/ts-proxy/src/main/kotlin/cn/tursom/proxy/annotation/ForFirstProxy.kt @@ -0,0 +1,17 @@ +package cn.tursom.proxy.annotation + +import kotlin.reflect.KClass + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +@Retention(AnnotationRetention.RUNTIME) +annotation class ForFirstProxy( + vararg val value: KClass<*> = [], + val name: String = "", + /** + * proxy objs must handle this method + * or will throw an exception + */ + val must: Boolean = false, + val errMsg: String = "", + val errClass: KClass = RuntimeException::class, +) \ No newline at end of file diff --git a/ts-core/ts-proxy/src/test/kotlin/cn/tursom/proxy/Example.kt b/ts-core/ts-proxy/src/test/kotlin/cn/tursom/proxy/Example.kt new file mode 100644 index 0000000..ae8a2b0 --- /dev/null +++ b/ts-core/ts-proxy/src/test/kotlin/cn/tursom/proxy/Example.kt @@ -0,0 +1,37 @@ +package cn.tursom.proxy + +import cn.tursom.proxy.Example.GetA +import cn.tursom.proxy.annotation.ForEachProxy +import net.sf.cglib.proxy.Enhancer +import org.junit.Test + +class Example { + open class TestClass protected constructor() : ProxyContainer { + @get:ForEachProxy + open val a: Int = 0 + override val proxy = ListProxy() + } + + fun interface GetA : ProxyMethod { + fun getA(): Int + } + + private val enhancer = Enhancer() + + init { + enhancer.setSuperclass(TestClass::class.java) + enhancer.setCallback(ProxyInterceptor()) + } + + @Test + fun test() { + val testClass = enhancer.create() as TestClass + testClass.proxy.addProxy(GetA { + println("on proxy method") + 0 + }) + + println(testClass.javaClass) + println(testClass.a) + } +} \ No newline at end of file