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