动态代理与Scala反射/Java反射在Java、Scala、Kotlin中的使用
一、Java反射
被代理对象
这个也是我使用graphql-java-codegen生成的一个resolver,这里我们需要了解这些,只知道我们有个接口,其中有个方法,需要被动态代理使用即可。
本文代码来自本人为graphql-java-codegen编写的集成测试客户端github-graphql-client,该仓库以github的graphql api为基准实现,针对三种语言进行测试。 https://github.com/jxnu-liguobin/github-graphql-client
还需要注意的是:本文的代理不是为了给方法添加前置或者后缀逻辑,而是直接替换方法本身实现。
public interface QueryResolver {
UserTO user(String login) throws Exception;
}
InvocationHandler实现
通常我们在Java中只需实现InvocationHandler接口,本文Scala和Kotlin同样使用该接口实现代理。如下:
final public class JavaResolverProxy implements InvocationHandler, JavaDeserializerAdapter {
private GraphQLResponseProjection projection;
private GraphQLOperationRequest request;
private final ServerConfig config;
public JavaResolverProxy(ServerConfig config, GraphQLResponseProjection projection, Class<? extends GraphQLOperationRequest> request) {
this.config = config;
this.projection = projection;
try {
this.request = request.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new ExecuteException("newInstance failed: ", e.getLocalizedMessage(), e);
}
}
public Object invoke(Object proxy, Method method, Object[] args) {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw new ExecuteException("invoke failed: ", t.getLocalizedMessage(), t);
}
} else {
return proxyInvoke(method, args);
}
}
private Object proxyInvoke(Method method, Object[] args) {
Field field = null;
List<GraphQLResponseField> fields;
Class<?> entityClass;
Type type = method.getGenericReturnType();
// 获取方法的返回类型,用于后续的json解析
if (type instanceof ParameterizedType) {
Type[] parameterizedType = ((ParameterizedType) type).getActualTypeArguments();
entityClass = (Class<?>) parameterizedType[0];
} else {
entityClass = (Class<?>)type;
}
if (isPrimitive(entityClass)) {
assert(projection == null);
} else {
assert(projection != null);
}
// 利用Java8的特性,获取参数名列表与参数值组成map,用于发送graphql请求,可以不管
List<Parameter> parameters = Arrays.stream(method.getParameters()).collect(Collectors.toList());
if (!parameters.isEmpty()) {
List<String> parameterNames = parameters.stream().map(Parameter::getName).collect(Collectors.toList());
List<Object> arguments = Arrays.stream(args).collect(Collectors.toList());
request.getInput().putAll(CollectionUtils.listToMap(parameterNames, arguments));
}
// 使用反射获取父类的字段
try {
field = projection.getClass().getSuperclass().getDeclaredField("fields");
field.setAccessible(true);
fields = (List<GraphQLResponseField>) field.get(projection);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new ExecuteException("access fields failed: ", e.getLocalizedMessage(), e);
} finally {
if (field != null) {
field.setAccessible(false);
}
}
//if fields not null, use it directly, because user want to select fields
if (projection != null && (fields == null || fields.isEmpty())) {
throw new ExecuteException("projection verification failed: ", "fields of projection cannot be empty", null);
}
GraphQLRequest graphQLRequest = new GraphQLRequest(request, projection);
Object ret;
// 发送graphql请求获取json解析后的对象
ret = OkHttp.syncRunQuery(config, graphQLRequest, entityClass, buildFunction3());
return ret;
}
}
代理实现
有了处理器的实现类,我们还需构造代理对象,如下:
final public class GitHubJavaClient {
private ServerConfig config;
private Class<?> resolver;
private GraphQLResponseProjection projection;
private Class<? extends GraphQLOperationRequest> request;
private GitHubJavaClient() {
}
private Object getResolver() {
JavaResolverProxy invocationHandler = new JavaResolverProxy(config, projection, request);
return Proxy.newProxyInstance(resolver.getClassLoader(), new Class[]{resolver}, invocationHandler);
}
private void setConfig(ServerConfig config) {
this.config = config;
}
private void setResolver(Class<?> resolver) {
this.resolver = resolver;
}
private void setRequest(Class<? extends GraphQLOperationRequest> request) {
this.request = request;
}
private void setProjection(GraphQLResponseProjection projection) {
this.projection = projection;
}
public static GitHubJavaClientBuilder newBuilder() {
return new GitHubJavaClientBuilder();
}
public static class GitHubJavaClientBuilder {
private GraphQLResponseProjection projection;
private Class<? extends GraphQLOperationRequest> request;
private ServerConfig config;
private GitHubJavaClientBuilder() {
}
public GitHubJavaClientBuilder setRequest(Class<? extends GraphQLOperationRequest> request) {
this.request = request;
return this;
}
public GitHubJavaClientBuilder setConfig(ServerConfig config) {
this.config = config;
return this;
}
public GitHubJavaClientBuilder setProjection(GraphQLResponseProjection projection) {
this.projection = projection;
return this;
}
"unchecked") (value =
public <Resolver> Resolver build(Class<Resolver> resolver) {
GitHubJavaClient invoke = new GitHubJavaClient();
assert (resolver != null);
assert (request != null);
invoke.setProjection(projection);
invoke.setResolver(resolver);
invoke.setConfig(config);
invoke.setRequest(request);
return (Resolver) invoke.getResolver();
}
}
}
使用
不需要new,直接调用接口的方法
public class JavaClientExample {
public static void main(String[] args) throws Exception {
// 1. Use projection to select the preset returned.
UserResponseProjection userResponseProjection = new UserResponseProjection().id().avatarUrl().login().resourcePath();
QueryResolver queryResolver = GitHubJavaClient.newBuilder()
// 2. Set the service endpoint.
.setConfig(ServerConfig.apply("https://api.github.com/graphql", Collections.singletonMap("Authorization", "Bearer xx")))
.setProjection(userResponseProjection)
// 3. Set the request corresponding to the resolver.
.setRequest(UserQueryRequest.class)
// 4. Set the resolver that needs a proxy.
.build(QueryResolver.class);
// 5. Use resolver to create a call.
UserTO userTO = queryResolver.user("jxnu-liguobin"); // projection and request must correspond to the return type of the user method.
System.out.println(userTO.toString());
}
}
二、Scala中使用Java反射
被代理对象
trait QueryResolver {
def user(login: String): UserTO
}
Scala中也可直接使用Java反射操作Java/Scala类,但是代码不如Scala反射优美。同时有些Scala特有类型可能反射不到,此时必须使用Scala反射。
InvocationHandler实现
//这里的Manifest是Scala特有,用于获取运行时类
final class ScalaResolverProxyV1[Request <: GraphQLOperationRequest : Manifest] private
(val config: ServerConfig, val projection: GraphQLResponseProjection) extends InvocationHandler
with JavaDeserializerAdapter {
private lazy val request: GraphQLOperationRequest = manifest[Request].runtimeClass.getConstructor(classOf[String]).
newInstance(null).asInstanceOf[GraphQLOperationRequest]
override def invoke(proxy: AnyRef, method: Method, args: Array[AnyRef]): Any =
if (classOf[AnyRef] == method.getDeclaringClass) {
try {
method.invoke(this, args)
} catch {
case t: Throwable =>
throw ExecuteException("invoke failed: ", t.getLocalizedMessage, t)
}
} else {
proxyInvoke(method, args)
}
private def proxyInvoke(method: Method, args: Array[AnyRef]): Any = {
val `type` = method.getGenericReturnType
val entityClass = `type` match {
case parameterizedType1: ParameterizedType =>
val parameterizedType = parameterizedType1.getActualTypeArguments
parameterizedType(0).asInstanceOf[Class[_]]
case _ => `type`.asInstanceOf[Class[_]]
}
if (isPrimitive(entityClass)) {
assert(projection == null)
} else {
assert(projection != null)
}
val parameters = method.getParameters.toList
if (parameters.nonEmpty) {
val parameterNames = parameters.map(_.getName)
val arguments = args.toList
request.getInput.putAll(CollectionUtils.listToMap(parameterNames, arguments))
}
// TODO remove reflect
var field: Field = null
var fields: util.List[GraphQLResponseField] = null
try {
field = projection.getClass.getSuperclass.getDeclaredField("fields")
field.setAccessible(true)
fields = field.get(projection).asInstanceOf[util.List[GraphQLResponseField]]
} catch {
case e@(_: NoSuchFieldException | _: IllegalAccessException) =>
throw ExecuteException("access fields failed: ", e.getLocalizedMessage, e)
} finally if (field != null) field.setAccessible(false)
if (projection != null && (fields == null || fields.isEmpty)) {
throw ExecuteException("projection verification failed: ", "fields of projection cannot be empty")
}
val graphQLRequest = new GraphQLRequest(request, projection)
OkHttp.syncRunQuery(config, graphQLRequest, entityClass)(extractData)
}
}
object ScalaResolverProxyV1 {
def apply[Request <: GraphQLOperationRequest : Manifest](config: ServerConfig, projection: GraphQLResponseProjection):
ScalaResolverProxyV1[Request] = new ScalaResolverProxyV1[Request](config, projection)
}
代理实现
class GithubScalaClient {
private var config: ServerConfig = _
private var resolver: Class[_] = _
private var projection: GraphQLResponseProjection = _
private def getResolverObjectV1[Request <: GraphQLOperationRequest : Manifest]: AnyRef = {
val invocationHandler: ScalaResolverProxyV1[Request] = ScalaResolverProxyV1[Request](config, projection)
Proxy.newProxyInstance(resolver.getClassLoader, Array[Class[_]](resolver), invocationHandler)
}
}
object GithubScalaClient {
def newBuilder: GitHubScalaClientBuilder = new GitHubScalaClientBuilder()
class GitHubScalaClientBuilder() {
private var projection: GraphQLResponseProjection = _
private var config: ServerConfig = _
def setConfig(config: ServerConfig): GitHubScalaClientBuilder = {
this.config = config
this
}
def setProjection(projection: GraphQLResponseProjection): GitHubScalaClientBuilder = {
this.projection = projection
this
}
def buildV1[Resolver: Manifest, Request <: GraphQLOperationRequest : Manifest]: Resolver = {
assert(this.config != null)
val invoke = new GithubScalaClient
invoke.config = this.config
invoke.projection = this.projection
invoke.resolver = manifest[Resolver].runtimeClass
invoke.getResolverObjectV1[Request].asInstanceOf[Resolver]
}
}
}
使用
object ScalaClientExample extends App {
val userResponseProjection = new UserResponseProjection().id().avatarUrl().login().resourcePath()
val config = ServerConfig("https://api.github.com/graphql", Map("Authorization" -> "Bearer xx"))
val queryResolver = GithubScalaClient.newBuilder.setConfig(config).
setProjection(userResponseProjection).
buildV1[QueryResolver, UserQueryRequest]
val userTO = queryResolver.user("jxnu-liguobin")
println(userTO.id) //tostring failed, because jackson use java Deserializer
}
三、Scala反射
被代理对象
trait QueryResolver {
def user(login: String): UserTO
}
InvocationHandler实现
// Scala反射的环境
import scala.reflect.runtime.{ universe => ru }
import scala.reflect.runtime.universe._
final class ScalaResolverProxyV2[Request <: GraphQLOperationRequest : Manifest, Out: Manifest] private //使用Manifest传递返回类型,不再需要通过返回获取方法的返回类型,json解析时,需要使用ScalaObjectMapper和CaseClassObjectMapper
(val config: ServerConfig, val projection: GraphQLResponseProjection) extends InvocationHandler
with ScalaDeserializer {
// 使用Scala反射获取构造函数
private[this] lazy val constructor = getRuntimeMirror.reflectClass(getRequestScalaType.typeSymbol.asClass)
.reflectConstructor(getRequestScalaType.members.find(_.isConstructor).get.asMethod)
private val request: GraphQLOperationRequest = constructor.apply(null).asInstanceOf[GraphQLOperationRequest]
//当前类的运行时环境
private[this] def getRuntimeMirror: ru.Mirror = runtimeMirror(getClass.getClassLoader)
//获取Request泛型的运行时类型,通过类型反射出构造函数
private[this] def getRequestScalaType: ru.Type = typeOf[Request]
override def invoke(proxy: AnyRef, method: Method, args: Array[AnyRef]): Any =
if (classOf[AnyRef] == method.getDeclaringClass) {
try {
method.invoke(this, args)
} catch {
case t: Throwable =>
throw ExecuteException("invoke failed: ", t.getLocalizedMessage, t)
}
} else {
proxyInvoke(method, args)
}
private def proxyInvoke(method: Method, args: Array[AnyRef]): Any = {
val `type` = method.getGenericReturnType
val isCollection = `type` match {
case _: ParameterizedType => true
case _ => false
}
if (isPrimitive(manifest[Out].runtimeClass)) {
assert(projection == null)
} else {
assert(projection != null)
}
val parameters = method.getParameters.toList
if (parameters.nonEmpty) {
val parameterNames = parameters.map(_.getName)
val arguments = args.toList
request.getInput.putAll(CollectionUtils.listToMap(parameterNames, arguments))
}
// use shapeless LabelledGeneric
// Scala反射获取父类的私有字段
def fieldValue(name: String): Any = {
//获取到当前类加载的运行时环境,反射出projection对象父类中的名为“fields”的字段
//members返回projection类的所有成员,包含继承过来的,所以这里可以拿到父类的“fields”字段
val im = ru.runtimeMirror(projection.getClass.getClassLoader)
getRuntimeMirror.classSymbol(projection.getClass).toType.members.filter(!_.isMethod).
filter(_.name.decodedName.toString.trim.equals(name)).map(s => {
im.reflect(projection).reflectField(s.asTerm).get
}).head
}
val fields: java.util.List[GraphQLResponseField] = fieldValue("fields").asInstanceOf[java.util.List[GraphQLResponseField]]
if (projection != null && (fields == null || fields.isEmpty)) {
throw ExecuteException("projection verification failed: ", "fields of projection cannot be empty")
}
val graphQLRequest = new GraphQLRequest(request, projection)
// extractData是一个解析函数,Out传递过去用于解析json。注意,这里没有entityClass
OkHttp.syncRunQuery(config, isCollection, graphQLRequest)(extractData[Out])
}
}
object ScalaResolverProxyV2 {
def apply[Request <: GraphQLOperationRequest : Manifest, Out: Manifest](config: ServerConfig, projection: GraphQLResponseProjection):
ScalaResolverProxyV2[Request, Out] = new ScalaResolverProxyV2[Request, Out](config, projection)
}
代理实现
其实只要在上面的GithubScalaClient中添加2个方法即可。
private def getResolverObjectV2[Request <: GraphQLOperationRequest : Manifest, Out: Manifest]: AnyRef = {
val invocationHandler: ScalaResolverProxyV2[Request, Out] = ScalaResolverProxyV2[Request, Out](config, projection)
Proxy.newProxyInstance(resolver.getClassLoader, Array[Class[_]](resolver), invocationHandler)
}
def buildV2[Resolver: Manifest, Request <: GraphQLOperationRequest : Manifest, Out: Manifest]: Resolver = {
assert(this.config != null)
val invoke = new GithubScalaClient
invoke.config = this.config
invoke.projection = this.projection
invoke.resolver = manifest[Resolver].runtimeClass
invoke.getResolverObjectV2[Request, Out].asInstanceOf[Resolver]
}
使用
由于我们使用了Manifest和ScalaObjectMapper,我们可以直接在调用时传递返回类型,这样就不需要使用反射获取方法的返回类型,减少一步操作,也更符合Scala代码风格。不过由于Jackson-scala-module提示不再支持Scala3,所以如果不能使用Manifest,那么就得继续使用entityClass。使用Manifest如下:
val userResponseProjection1 = new UserResponseProjection().id().avatarUrl().login().resourcePath()
val queryResolver1 = GithubScalaClient.newBuilder.setConfig(config).
setProjection(userResponseProjection).
buildV2[QueryResolver, UserQueryRequest, UserTO]//指定返回类型
val userTO1 = queryResolver1.user("jxnu-liguobin")
println(userTO.toString())
当然,动态代理+JSON的最大问题之一是无法处理编译时的类型不匹配,只会在运行时暴露出来错误。本文不考虑性能问题,专注可行性,主要用于测试代码生成库graphql-java-codegen。
四、Kotlin中使用Java反射
被代理对象
interface QueryResolver {
fun rateLimit(dryRun: Boolean?): RateLimitTO?
}
具体就不写了,坑也很多,三种语言的源码在github-graphql-client,
使用
object KotlinClientExample {
fun main(args: Array<String>) {
// Since Kotlin has a mandatory non-null for fields, a field-less interface test is used here
val rateLimitResponseProjection = RateLimitResponseProjection().`all$`(1)
val queryResolver = GithubKotlinClient.newBuilder()
.setConfig(
ServerConfigAdapter(
"https://api.github.com/graphql",
mapOf(Pair("Authorization", "Bearer xx"))
)
)
.setProjection(rateLimitResponseProjection).build<QueryResolver, RateLimitQueryRequest>()
val rateLimit = queryResolver.rateLimit(true)
println(rateLimit.toString())
}
}