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