001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2025 microBean™. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with 006 * the License. You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 011 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 012 * specific language governing permissions and limitations under the License. 013 */ 014package org.microbean.clientproxy.bytebuddy; 015 016import java.util.Collection; 017import java.util.List; 018import java.util.Objects; 019 020import net.bytebuddy.ByteBuddy; 021 022import net.bytebuddy.description.method.MethodDescription; 023import net.bytebuddy.description.method.ParameterDescription; 024 025import net.bytebuddy.description.modifier.FieldManifestation; 026import net.bytebuddy.description.modifier.MethodManifestation; 027import net.bytebuddy.description.modifier.ParameterManifestation; 028import net.bytebuddy.description.modifier.TypeManifestation; 029 030import net.bytebuddy.description.type.TypeDefinition; 031import net.bytebuddy.description.type.TypeDescription; 032 033import net.bytebuddy.dynamic.DynamicType; 034 035import net.bytebuddy.implementation.DefaultMethodCall; 036import net.bytebuddy.implementation.HashCodeMethod; 037import net.bytebuddy.implementation.EqualsMethod; 038import net.bytebuddy.implementation.FieldAccessor; 039import net.bytebuddy.implementation.MethodCall; 040 041import net.bytebuddy.implementation.bytecode.assign.Assigner; 042 043import net.bytebuddy.matcher.ElementMatcher; 044 045import net.bytebuddy.pool.TypePool; 046 047import static net.bytebuddy.description.modifier.Ownership.STATIC; 048import static net.bytebuddy.description.modifier.SyntheticState.SYNTHETIC; 049import static net.bytebuddy.description.modifier.Visibility.PRIVATE; 050import static net.bytebuddy.description.modifier.Visibility.PUBLIC; 051 052import static net.bytebuddy.description.type.TypeDescription.Generic.Builder.parameterizedType; 053import static net.bytebuddy.description.type.TypeDescription.Generic.Builder.typeVariable; 054 055import static net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy.Default.NO_CONSTRUCTORS; 056 057import static net.bytebuddy.implementation.MethodCall.invoke; 058import static net.bytebuddy.implementation.MethodCall.invokeSelf; 059 060import static net.bytebuddy.matcher.ElementMatchers.any; 061import static net.bytebuddy.matcher.ElementMatchers.hasParameters; 062import static net.bytebuddy.matcher.ElementMatchers.isConstructor; 063import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy; 064import static net.bytebuddy.matcher.ElementMatchers.isEquals; 065import static net.bytebuddy.matcher.ElementMatchers.isFinal; 066import static net.bytebuddy.matcher.ElementMatchers.isHashCode; 067import static net.bytebuddy.matcher.ElementMatchers.isPackagePrivate; 068import static net.bytebuddy.matcher.ElementMatchers.isPublic; 069import static net.bytebuddy.matcher.ElementMatchers.isToString; 070import static net.bytebuddy.matcher.ElementMatchers.isVirtual; 071import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; 072import static net.bytebuddy.matcher.ElementMatchers.named; 073import static net.bytebuddy.matcher.ElementMatchers.not; 074import static net.bytebuddy.matcher.ElementMatchers.returns; 075import static net.bytebuddy.matcher.ElementMatchers.takesArgument; 076import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments; 077 078/** 079 * An class generator that uses <a href="https://bytebuddy.net/#/">Byte Buddy</a> to {@linkplain #generate(String, 080 * TypeDefinition, Collection) generate} {@linkplain org.microbean.reference.ClientProxy client proxy} classes. 081 * 082 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a> 083 */ 084public final class BBClientProxyClassGenerator { 085 086 private final TypePool typePool; 087 088 /** 089 * Creates a new {@link BBClientProxyClassGenerator}. 090 * 091 * @param typePool a {@link TypePool} (normally a {@link TypeElementTypePool}); must not be {@code null} 092 * 093 * @exception NullPointerException if {@code typePool} is {@code null} 094 */ 095 public BBClientProxyClassGenerator(final TypePool typePool) { 096 super(); 097 this.typePool = Objects.requireNonNull(typePool, "typePool"); 098 } 099 100 /** 101 * Creates and returns a new {@link DynamicType.Unloaded} representing a client proxy class. 102 * 103 * @param name the name of the client proxy class; must not be {@code null}; must be a valid Java class <a 104 * href="https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/lang/ClassLoader.html#binary-name">binary 105 * name</a> 106 * 107 * @param superclass a {@link TypeDefinition} representing a superclass; must not be {@code null} 108 * 109 * @param interfaces a {@link Collection} of {@link TypeDefinition}s representing interfaces the client proxy class 110 * will implement; must not be {@code null} 111 * 112 * @return a new, non-{@code null} {@link DynamicType.Unloaded} representing a client proxy class 113 * 114 * @exception NullPointerException if any argument is {@code null} 115 */ 116 public final DynamicType.Unloaded<?> generate(final String name, 117 final TypeDefinition superclass, 118 final Collection<? extends TypeDefinition> interfaces) { 119 120 // ClientProxy<Superclass> 121 final TypeDescription.Generic clientProxyType = 122 parameterizedType(this.typeDescription("org.microbean.reference.ClientProxy"), 123 List.of(superclass)) 124 .build(); 125 126 // Supplier<? extends Superclass> 127 final TypeDescription.Generic supplierType = 128 parameterizedType(this.typeDescription("java.util.function.Supplier"), 129 List.of(TypeDescription.Generic.Builder.of(superclass.asGenericType()).asWildcardUpperBound())) 130 .build(); 131 132 // public final class Name extends Superclass implements ClientProxy<Superclass>, Interfaces { /* ... */ } 133 DynamicType.Builder<?> builder = new ByteBuddy() 134 .subclass(superclass, NO_CONSTRUCTORS) 135 .merge(List.of(PUBLIC, SYNTHETIC, TypeManifestation.FINAL)) 136 .name(name) 137 .implement(clientProxyType) 138 .implement(interfaces) 139 140 // private final Supplier<? extends Superclass> $proxiedSupplier; 141 .defineField("$proxiedSupplier", supplierType, PRIVATE, SYNTHETIC, FieldManifestation.FINAL) 142 143 // public Name(final Supplier<? extends Superclass> proxiedSupplier) { 144 // super(); 145 // Objects.requireNonNull(proxiedSupplier, "proxiedSupplier"); 146 // this.$proxiedSupplier = proxiedSupplier; 147 // } 148 .defineConstructor(PUBLIC, SYNTHETIC) 149 .withParameter(supplierType, "proxiedSupplier", ParameterManifestation.FINAL) 150 .intercept(invoke(superclass.getDeclaredMethods().filter(isConstructor().and(takesNoArguments())).getOnly()) 151 .andThen(invoke(this.typeDescription("java.util.Objects") 152 .getDeclaredMethods() 153 .filter(named("requireNonNull") 154 .and(takesArgument(1, this.typeDescription("java.lang.String")))) 155 .getOnly()) 156 .withArgument(0) 157 .with("proxiedSupplier")) 158 .andThen(FieldAccessor.ofField("$proxiedSupplier").setsArgumentAt(0))) 159 160 // @Override // ClientProxy<Superclass> 161 // public final Superclass $proxied() { 162 // return this.$proxiedSupplier.get(); 163 // } 164 .defineMethod("$proxied", superclass, PUBLIC, SYNTHETIC, MethodManifestation.FINAL) 165 .intercept(invoke(named("get")) 166 .onField("$proxiedSupplier") 167 .withAssigner(Assigner.DEFAULT, Assigner.Typing.DYNAMIC)) 168 169 // @Override // ClientProxy<Superclass> 170 // public final Superclass $cast() { 171 // return ClientProxy.super.$cast(); 172 // } 173 .defineMethod("$cast", superclass, PUBLIC, SYNTHETIC, MethodManifestation.FINAL) 174 .intercept(DefaultMethodCall.prioritize(clientProxyType.asErasure())) 175 176 // Existing/inherited methods; remember that they form a stack, so the last .method() call below should be the 177 // most specific. See https://bytebuddy.net/#members for details. 178 179 // @Override // Superclass/interfaces 180 // public Bar foo() { 181 // return $proxied().foo(); // so long as foo() meets certain requirements 182 // } 183 .method(isBusinessMethod() 184 .and(not(isJavaDeclaredMethod() 185 .and(isPackagePrivate() 186 .or(hasOnePackagePrivateParameter()))))) 187 .intercept(invokeSelf() 188 .onMethodCall(invoke(named("$proxied"))) 189 .withAllArguments()) 190 191 // @Override // Superclass, Object 192 // public final boolean equals(final Object other) { 193 // if (other == this) { 194 // return true; 195 // } else if (other != null && other.getClass() == this.getClass()) { 196 // return this.$proxiedSupplier == ((Name)other).$proxiedSupplier; 197 // } else { 198 // return false; 199 // } 200 // } 201 .method(isEquals()) 202 .intercept(EqualsMethod.isolated() 203 .withIdentityFields(any()) // there's only one 204 .withNonNullableFields(any())) 205 206 // @Override // Superclass, Object 207 // public int hashCode() { 208 // int offset = 31; 209 // return offset * 17 + this.$proxiedSupplier.hashCode(); // or similar 210 // } 211 .method(isHashCode()) 212 .intercept(HashCodeMethod.usingOffset(31) 213 .withIdentityFields(any()) 214 .withNonNullableFields(any()) 215 .withMultiplier(17)) // see https://github.com/raphw/byte-buddy/issues/1764 216 217 // @Override // Superclass/interfaces/Object 218 // public String toString() { 219 // return $proxied().toString(); 220 // } 221 .method(isToString()) 222 .intercept(invoke(named("toString")) 223 .onMethodCall(invoke(named("$proxied")))); 224 225 return builder.make(this.typePool); 226 } 227 228 229 /* 230 * Static methods. 231 */ 232 233 234 private static final ElementMatcher<MethodDescription> hasOnePackagePrivateParameter() { 235 return m -> { 236 for (final ParameterDescription pd : m.getParameters()) { 237 if (isPackagePrivate().matches(pd.getType())) { 238 return true; 239 } 240 } 241 return false; 242 }; 243 } 244 245 private static final ElementMatcher.Junction<MethodDescription> isBusinessMethod() { 246 return isVirtual() 247 .and(not(isFinal())) 248 .and(not(isDeclaredBy(Object.class))); 249 } 250 251 private static final ElementMatcher.Junction<MethodDescription> isJavaDeclaredMethod() { 252 return isDeclaredBy(typeNameStartsWith("java.")); 253 } 254 255 private final TypeDescription typeDescription(final String canonicalName) { 256 return this.typePool.describe(canonicalName).resolve(); 257 } 258 259 private static final ElementMatcher<TypeDefinition> typeNameStartsWith(final String prefix) { 260 Objects.requireNonNull(prefix, "prefix"); 261 return t -> t.getTypeName().startsWith(prefix); 262 } 263 264}