clazz) {
89 | return clazz.newInstance();
90 | }
91 |
92 | // added bridge method
93 | public FooSubType createFoo(Class clazz) {
94 | invokevirtual this.createFoo(java/lang/Class)LFoo
95 | checkcast FooSubType
96 | areturn
97 | }
98 | ```
99 |
100 | ## Adapter methods
101 |
102 | In extreme cases, we can add a method whose return type has nothing to do with the return type of the declared method.
103 | For example, if you have the following code:
104 |
105 | ```java
106 | @WithBridgeMethods(value = String.class, adapterMethod = "convert")
107 | public URL getURL() {
108 | URL url = ....
109 | return url;
110 | }
111 |
112 | private Object convert(URL url, Class targetType) {
113 | return url.toString();
114 | }
115 | ```
116 |
117 | The Maven mojo will insert the following bridge method:
118 |
119 | ```java
120 | public String getURL() {
121 | return (String) convert(getURL(), String.class); // invokeVirtual to getURL that returns URL
122 | }
123 | ```
124 |
125 | The specified adapter method must be a method specified on the current class or its ancestors.
126 | It cannot be a static method.
127 |
128 | ## Bridge methods and interfaces
129 |
130 | You can use `@WithBridgeMethods` with interfaces, too. However, making this work correctly is tricky,
131 | as you have to ensure that bridge methods are implemented on all the classes that implement the interface,
132 | for example by adding `@WithBridgeMethods` on every implementation of the method in question,
133 | or by introducing a base class that provides a bridge method.
134 |
135 | For adapter methods, the bridge method annotation on the interface does not need to declare the
136 | adapter method, but the bridge method annotation on the implementation does.
137 |
138 | See the Javadoc for more details:
139 |
140 | - [`bridge-method-annotation`](https://javadoc.jenkins.io/component/bridge-method-annotation/)
141 | - [`bridge-method-injector`](https://javadoc.jenkins.io/component/bridge-method-injector/)
142 |
143 | ## Maven repository
144 |
145 | Starting with 1.25, this library is published at:
146 |
147 | ```xml
148 |
149 | repo.jenkins-ci.org
150 | https://repo.jenkins-ci.org/releases/
151 |
152 | ```
153 |
154 | ## Java support
155 |
156 | Starting with 1.25, this library requires Java 11 or newer.
157 |
158 | ## Integration into your build
159 |
160 | Add the following dependency in your POM. (This dependency is not needed at runtime, but it is necessary
161 | for compilation of source code that transitively depend on this, so it is the simplest to just treat
162 | this like a regular library dependency)
163 |
164 | ```xml
165 |
166 | com.infradna.tool
167 | bridge-method-annotation
168 | 1.25
169 |
170 | ```
171 |
172 | Then put the following fragment in your build to have the byte-code post processor kick in to inject the necessary bridge methods.
173 |
174 | ```xml
175 |
176 |
177 |
178 | com.infradna.tool
179 | bridge-method-injector
180 | 1.25
181 |
182 |
183 |
184 | process
185 |
186 |
187 |
188 |
189 |
190 |
191 | ```
192 |
--------------------------------------------------------------------------------
/annotation/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | com.infradna.tool
6 | bridge-method-injector-parent
7 | ${revision}${changelist}
8 |
9 | bridge-method-annotation
10 | Bridge method injection annotations
11 |
12 |
13 |
14 | org.jenkins-ci
15 | annotation-indexer
16 | 1.18
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/annotation/src/main/java/com/infradna/tool/bridge_method_injector/BridgeMethodsAdded.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2010, InfraDNA, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 | package com.infradna.tool.bridge_method_injector;
25 |
26 | import java.lang.annotation.ElementType;
27 | import java.lang.annotation.Retention;
28 | import java.lang.annotation.RetentionPolicy;
29 | import java.lang.annotation.Target;
30 |
31 | /**
32 | * This annotation is added after the class transformation to indicate that
33 | * the class has already been processed by bridge method injector.
34 | *
35 | *
36 | * Used for up-to-date check to avoid unnecessary transformations.
37 | *
38 | * @author Kohsuke Kawaguchi
39 | */
40 | @Retention(RetentionPolicy.CLASS)
41 | @Target(ElementType.TYPE)
42 | public @interface BridgeMethodsAdded {}
43 |
--------------------------------------------------------------------------------
/annotation/src/main/java/com/infradna/tool/bridge_method_injector/WithBridgeMethods.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2010, InfraDNA, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 | package com.infradna.tool.bridge_method_injector;
25 |
26 | import java.lang.annotation.Documented;
27 | import java.lang.annotation.ElementType;
28 | import java.lang.annotation.Retention;
29 | import java.lang.annotation.RetentionPolicy;
30 | import java.lang.annotation.Target;
31 | import org.jvnet.hudson.annotation_indexer.Indexed;
32 |
33 | /**
34 | * Request that bridge methods of the same name and same arguments be generated
35 | * with each specified type as the return type. This helps you maintain binary compatibility
36 | * as you evolve your classes.
37 | *
38 | *
39 | * For example, if you have the following code:
40 | *
41 | *
42 | * @WithBridgeMethods(Foo.class)
43 | * public FooSubType getFoo() { ... }
44 | *
45 | *
46 | *
47 | * The Maven mojo will insert the following bridge method:
48 | *
49 | *
50 | * public Foo getFoo() {
51 | * return getFoo(); // invokevirtual to getFoo() that returns FooSubType
52 | * }
53 | *
54 | *
55 | *
56 | * In some cases, it's necessary to widen the return type of a method, but in a way that legacy
57 | * calls would still return instances of the original type. In this case, add
58 | * {@link #castRequired() castRequired=true} to the annotation. For example, if you have the
59 | * following code:
60 | *
61 | * @WithBridgeMethods(value=FooSubType.class, castRequired=true)
62 | * public <T extends Foo> createFoo(Class<T> clazz) {
63 | * return clazz.newInstance();
64 | * }
65 | *
66 | *
67 | * The Maven mojo will insert the following bridge method:
68 | *
69 | *
70 | * public FooSubType createFoo(Class clazz) {
71 | * return (FooSubType) createFoo(clazz); // invokeVirtual to createFoo that returns Foo
72 | * }
73 | *
74 | *
75 | *
76 | * In extreme cases, this method can add a method whose return type has nothing to do
77 | * with the return type of the declared method. For example, if you have the following code:
78 | *
79 | *
80 | * @WithBridgeMethods(value=String.class, adapterMethod="convert")
81 | * public URL getURL() {
82 | * URL url = ....
83 | * return url;
84 | * }
85 | *
86 | * private Object convert(URL url, Class targetType) { return url.toString(); }
87 | *
88 | *
89 | *
90 | * The Maven mojo will insert the following bridge method:
91 | *
92 | *
93 | * public String getURL() {
94 | * return (String)convert(getURL(),String.class); // invokeVirtual to getURL that returns URL
95 | * }
96 | *
97 | *
98 | *
99 | * The specified adapter method must be a method specified on the current class
100 | * or its ancestors. It cannot be a static method.
101 | *
102 | * @author Kohsuke Kawaguchi
103 | */
104 | @Retention(RetentionPolicy.CLASS)
105 | @Target(ElementType.METHOD)
106 | @Documented
107 | @Indexed
108 | public @interface WithBridgeMethods {
109 | /**
110 | * Specifies the return types. These types must be assignable from the actual
111 | * method return type, or {@link #castRequired()} should be set to true.
112 | */
113 | Class>[] value();
114 |
115 | /**
116 | * Specifies whether the injected bridge methods should perform a cast prior to returning. Only
117 | * set this to true when it is known that calls to the bridge methods will in fact return
118 | * objects assignable to {@linkplain #value() the bridge method return type}, even though
119 | * the declared method return type is not assignable to them.
120 | *
121 | * @since 1.4
122 | */
123 | boolean castRequired() default false;
124 |
125 | /**
126 | * Specifies the method to convert return value. This lets bridge methods to return
127 | * any types, even if it's unrelated to the return type of the declared method.
128 | *
129 | * @since 1.14
130 | */
131 | String adapterMethod() default "";
132 | }
133 |
--------------------------------------------------------------------------------
/injector/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | com.infradna.tool
6 | bridge-method-injector-parent
7 | ${revision}${changelist}
8 |
9 |
10 | bridge-method-injector
11 | maven-plugin
12 | bridge-method-injector
13 | Evolve your classes without breaking compatibility
14 |
15 |
16 | ${maven.version}
17 |
18 |
19 |
20 | 9.8
21 | 3.15.1
22 | 3.9.6
23 |
24 |
25 |
26 |
27 | ${project.groupId}
28 | bridge-method-annotation
29 | ${project.version}
30 |
31 |
32 | com.github.spotbugs
33 | spotbugs-annotations
34 | true
35 |
36 |
37 | com.google.code.findbugs
38 | jsr305
39 |
40 |
41 |
42 |
43 | org.ow2.asm
44 | asm
45 | ${asm.version}
46 |
47 |
48 | org.ow2.asm
49 | asm-commons
50 | ${asm.version}
51 |
52 |
53 | org.apache.maven
54 | maven-plugin-api
55 | ${maven.version}
56 | provided
57 |
58 |
59 | org.apache.maven.plugin-tools
60 | maven-plugin-annotations
61 | ${maven-plugin-tools.version}
62 | provided
63 |
64 |
65 | junit
66 | junit
67 | test
68 |
69 |
70 |
71 |
72 |
73 |
74 | org.apache.maven.plugins
75 | maven-plugin-plugin
76 | ${maven-plugin-tools.version}
77 |
78 | bridge-method-injector
79 |
80 |
81 |
82 | maven-antrun-plugin
83 |
84 |
85 |
86 | run
87 |
88 | test
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
--------------------------------------------------------------------------------
/injector/src/main/java/com/infradna/tool/bridge_method_injector/ClassAnnotationInjector.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2010, InfraDNA, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 | package com.infradna.tool.bridge_method_injector;
25 |
26 | import org.objectweb.asm.ClassVisitor;
27 | import org.objectweb.asm.FieldVisitor;
28 | import org.objectweb.asm.MethodVisitor;
29 | import org.objectweb.asm.Opcodes;
30 |
31 | /**
32 | * Adds class annotations.
33 | *
34 | * @author Kohsuke Kawaguchi
35 | */
36 | abstract class ClassAnnotationInjector extends ClassVisitor {
37 | ClassAnnotationInjector(ClassVisitor cv) {
38 | super(Opcodes.ASM9, cv);
39 | }
40 |
41 | private boolean emitted = false;
42 |
43 | private void maybeEmit() {
44 | if (!emitted) {
45 | emitted = true;
46 | emit();
47 | }
48 | }
49 |
50 | protected abstract void emit();
51 |
52 | @Override
53 | public void visitInnerClass(String name, String outerName, String innerName, int access) {
54 | maybeEmit();
55 | super.visitInnerClass(name, outerName, innerName, access);
56 | }
57 |
58 | @Override
59 | public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
60 | maybeEmit();
61 | return super.visitField(access, name, desc, signature, value);
62 | }
63 |
64 | @Override
65 | public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
66 | maybeEmit();
67 | return super.visitMethod(access, name, desc, signature, exceptions);
68 | }
69 |
70 | @Override
71 | public void visitEnd() {
72 | maybeEmit();
73 | super.visitEnd();
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/injector/src/main/java/com/infradna/tool/bridge_method_injector/MethodInjector.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2010, InfraDNA, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 | package com.infradna.tool.bridge_method_injector;
25 |
26 | import static org.objectweb.asm.ClassWriter.COMPUTE_MAXS;
27 | import static org.objectweb.asm.Opcodes.ACC_ABSTRACT;
28 | import static org.objectweb.asm.Opcodes.ACC_BRIDGE;
29 | import static org.objectweb.asm.Opcodes.ACC_STATIC;
30 | import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
31 | import static org.objectweb.asm.Opcodes.ILOAD;
32 | import static org.objectweb.asm.Opcodes.INVOKESTATIC;
33 | import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
34 | import static org.objectweb.asm.Opcodes.IRETURN;
35 | import static org.objectweb.asm.Opcodes.POP;
36 | import static org.objectweb.asm.Opcodes.POP2;
37 |
38 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
39 | import java.io.BufferedInputStream;
40 | import java.io.File;
41 | import java.io.FileInputStream;
42 | import java.io.FileOutputStream;
43 | import java.io.IOException;
44 | import java.util.ArrayList;
45 | import java.util.List;
46 | import org.objectweb.asm.AnnotationVisitor;
47 | import org.objectweb.asm.ClassReader;
48 | import org.objectweb.asm.ClassVisitor;
49 | import org.objectweb.asm.ClassWriter;
50 | import org.objectweb.asm.MethodVisitor;
51 | import org.objectweb.asm.Opcodes;
52 | import org.objectweb.asm.Type;
53 | import org.objectweb.asm.commons.GeneratorAdapter;
54 |
55 | /**
56 | * Injects bridge methods as per {@link WithBridgeMethods}.
57 | *
58 | * @author Kohsuke Kawaguchi
59 | */
60 | public class MethodInjector {
61 | public void handleRecursively(File f) throws IOException {
62 | if (f.isDirectory()) {
63 | File[] files = f.listFiles();
64 | if (files != null) {
65 | for (File c : files) {
66 | handleRecursively(c);
67 | }
68 | }
69 | } else if (f.getName().endsWith(".class")) {
70 | handle(f);
71 | }
72 | }
73 |
74 | public void handle(File classFile) throws IOException {
75 | byte[] image;
76 | try (FileInputStream in = new FileInputStream(classFile);
77 | BufferedInputStream bis = new BufferedInputStream(in)) {
78 | ClassReader cr = new ClassReader(bis);
79 | ClassWriter cw = new ClassWriter(cr, COMPUTE_MAXS);
80 | cr.accept(new Transformer(new ClassAnnotationInjectorImpl(cw)), 0);
81 | image = cw.toByteArray();
82 | } catch (AlreadyUpToDate unused) {
83 | // no need to process this class. it's already up-to-date.
84 | return;
85 | } catch (IOException | RuntimeException e) {
86 | throw new IOException("Failed to process " + classFile, e);
87 | }
88 |
89 | // write it back
90 | try (FileOutputStream out = new FileOutputStream(classFile)) {
91 | out.write(image);
92 | }
93 | }
94 |
95 | /**
96 | * Thrown to indicate that there's no need to re-process this class file.
97 | */
98 | static class AlreadyUpToDate extends RuntimeException {
99 | private static final long serialVersionUID = 1L;
100 | }
101 |
102 | static class ClassAnnotationInjectorImpl extends ClassAnnotationInjector {
103 | ClassAnnotationInjectorImpl(ClassVisitor cv) {
104 | super(cv);
105 | }
106 |
107 | @Override
108 | protected void emit() {
109 | AnnotationVisitor av = cv.visitAnnotation(SYNTHETIC_METHODS_ADDED, false);
110 | av.visitEnd();
111 | }
112 | }
113 |
114 | private static class WithBridgeMethodsAnnotationVisitor extends AnnotationVisitor {
115 | protected boolean castRequired = false;
116 | protected String adapterMethod = null;
117 | protected final List types = new ArrayList<>();
118 |
119 | public WithBridgeMethodsAnnotationVisitor(AnnotationVisitor av) {
120 | super(Opcodes.ASM9, av);
121 | }
122 |
123 | @Override
124 | public AnnotationVisitor visitArray(String name) {
125 | return new AnnotationVisitor(Opcodes.ASM9, super.visitArray(name)) {
126 |
127 | @Override
128 | public void visit(String name, Object value) {
129 | if (value instanceof Type) {
130 | // assume this is a member of the array of classes named "value" in WithBridgeMethods
131 | types.add((Type) value);
132 | }
133 | super.visit(name, value);
134 | }
135 | };
136 | }
137 |
138 | @Override
139 | public void visit(String name, Object value) {
140 | if ("castRequired".equals(name) && value instanceof Boolean) {
141 | castRequired = (Boolean) value;
142 | }
143 | if ("adapterMethod".equals(name) && value instanceof String) {
144 | adapterMethod = (String) value;
145 | }
146 | super.visit(name, value);
147 | }
148 | }
149 |
150 | static class Transformer extends ClassVisitor {
151 | private String internalClassName;
152 | /**
153 | * Synthetic methods to be generated.
154 | */
155 | private final List syntheticMethods = new ArrayList<>();
156 |
157 | class SyntheticMethod {
158 | final int access;
159 | final String name;
160 | final String desc;
161 | final String originalSignature;
162 | final String[] exceptions;
163 | final boolean castRequired;
164 | final String adapterMethod;
165 |
166 | /**
167 | * Return type of the bridge method to be inserted.
168 | */
169 | final Type returnType;
170 | /**
171 | * Return type of the declared method written in the source code.
172 | */
173 | final Type originalReturnType;
174 |
175 | SyntheticMethod(
176 | int access,
177 | String name,
178 | String desc,
179 | String originalSignature,
180 | String[] exceptions,
181 | Type returnType,
182 | boolean castRequired,
183 | String adapterMethod) {
184 | this.access = access;
185 | this.name = name;
186 | this.desc = desc;
187 | this.originalSignature = originalSignature;
188 | this.exceptions = exceptions;
189 | this.returnType = returnType;
190 | this.castRequired = castRequired;
191 | this.adapterMethod = adapterMethod;
192 | originalReturnType = Type.getReturnType(desc);
193 | }
194 |
195 | /**
196 | * Injects a synthetic method and send it to cv.
197 | */
198 | public void inject(ClassVisitor cv) {
199 | Type[] paramTypes = Type.getArgumentTypes(desc);
200 |
201 | int access = this.access | ACC_SYNTHETIC | ACC_BRIDGE;
202 | String methodDescriptor = Type.getMethodDescriptor(returnType, paramTypes);
203 | MethodVisitor mv = cv.visitMethod(
204 | access, name, methodDescriptor, null /*TODO:is this really correct?*/, exceptions);
205 | if ((access & ACC_ABSTRACT) == 0) {
206 | GeneratorAdapter ga = new GeneratorAdapter(mv, access, name, methodDescriptor);
207 | mv.visitCode();
208 |
209 | int sz = 0;
210 |
211 | if (hasAdapterMethod()) {
212 | // the LHS of the adapter method invocation
213 | ga.loadThis();
214 | sz++;
215 | }
216 |
217 | boolean isStatic = (access & ACC_STATIC) != 0;
218 | if (!isStatic) {
219 | ga.loadThis();
220 | sz++;
221 | }
222 |
223 | int argpos = 0;
224 | for (Type p : paramTypes) {
225 | mv.visitVarInsn(p.getOpcode(ILOAD), argpos + (isStatic ? 0 : 1));
226 | argpos += p.getSize();
227 | }
228 | sz += argpos;
229 |
230 | mv.visitMethodInsn(isStatic ? INVOKESTATIC : INVOKEVIRTUAL, internalClassName, name, desc, false);
231 | if (hasAdapterMethod()) {
232 | insertAdapterMethod(ga);
233 | } else if (castRequired || returnType.equals(Type.VOID_TYPE)) {
234 | ga.unbox(returnType);
235 | } else {
236 | ga.box(originalReturnType);
237 | }
238 | if (returnType.equals(Type.VOID_TYPE)
239 | || returnType.getClassName().equals("java.lang.Void")) {
240 | // bridge to void, which means disregard the return value from the original method
241 | switch (originalReturnType.getSize()) {
242 | case 0:
243 | throw new IllegalArgumentException("Cannot bridge " + name
244 | + " from void to void; did you mean to use a different type?");
245 | case 1:
246 | mv.visitInsn(POP);
247 | break;
248 | case 2:
249 | mv.visitInsn(POP2);
250 | break;
251 | default:
252 | throw new AssertionError("Unexpected operand size: " + originalReturnType);
253 | }
254 | }
255 | mv.visitInsn(returnType.getOpcode(IRETURN));
256 | mv.visitMaxs(sz, 0);
257 | }
258 | mv.visitEnd();
259 | }
260 |
261 | private boolean hasAdapterMethod() {
262 | return adapterMethod != null && adapterMethod.length() > 0;
263 | }
264 |
265 | private void insertAdapterMethod(GeneratorAdapter ga) {
266 | ga.push(returnType);
267 | ga.visitMethodInsn(
268 | INVOKEVIRTUAL,
269 | internalClassName,
270 | adapterMethod,
271 | Type.getMethodDescriptor(
272 | Type.getType(Object.class), // return type
273 | originalReturnType,
274 | Type.getType(Class.class)),
275 | false);
276 | ga.unbox(returnType);
277 | }
278 | }
279 |
280 | Transformer(ClassVisitor cv) {
281 | super(Opcodes.ASM9, cv);
282 | }
283 |
284 | @Override
285 | public void visit(
286 | int version, int access, String name, String signature, String superName, String[] interfaces) {
287 | this.internalClassName = name;
288 | super.visit(version, access, name, signature, superName, interfaces);
289 | }
290 |
291 | @Override
292 | public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
293 | if (desc.equals(SYNTHETIC_METHODS_ADDED)) {
294 | throw new AlreadyUpToDate(); // no need to process this class
295 | }
296 | return super.visitAnnotation(desc, visible);
297 | }
298 |
299 | /**
300 | * Look for methods annotated with {@link WithBridgeMethods}.
301 | */
302 | @Override
303 | public MethodVisitor visitMethod(
304 | final int access,
305 | final String name,
306 | final String mdesc,
307 | final String signature,
308 | final String[] exceptions) {
309 | MethodVisitor mv = super.visitMethod(access, name, mdesc, signature, exceptions);
310 | return new MethodVisitor(Opcodes.ASM9, mv) {
311 | @Override
312 | public AnnotationVisitor visitAnnotation(String adesc, boolean visible) {
313 | AnnotationVisitor av = super.visitAnnotation(adesc, visible);
314 | if (adesc.equals(WITH_SYNTHETIC_METHODS) && (access & ACC_SYNTHETIC) == 0) {
315 | return new WithBridgeMethodsAnnotationVisitor(av) {
316 |
317 | @Override
318 | public void visitEnd() {
319 | super.visitEnd();
320 | for (Type type : this.types) {
321 | syntheticMethods.add(new SyntheticMethod(
322 | access,
323 | name,
324 | mdesc,
325 | signature,
326 | exceptions,
327 | type,
328 | this.castRequired,
329 | this.adapterMethod));
330 | }
331 | }
332 | };
333 | }
334 | return av;
335 | }
336 | };
337 | }
338 |
339 | /**
340 | * Inject methods at the end.
341 | */
342 | @Override
343 | public void visitEnd() {
344 | for (SyntheticMethod m : syntheticMethods) {
345 | m.inject(cv);
346 | }
347 | super.visitEnd();
348 | }
349 | }
350 |
351 | @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "user-provided value for running the program")
352 | public static void main(String[] args) throws IOException {
353 | MethodInjector mi = new MethodInjector();
354 | for (String a : args) {
355 | mi.handleRecursively(new File(a));
356 | }
357 | }
358 |
359 | private static final String SYNTHETIC_METHODS_ADDED = Type.getDescriptor(BridgeMethodsAdded.class);
360 | private static final String WITH_SYNTHETIC_METHODS = Type.getDescriptor(WithBridgeMethods.class);
361 | }
362 |
--------------------------------------------------------------------------------
/injector/src/main/java/com/infradna/tool/bridge_method_injector/ProcessMojo.java:
--------------------------------------------------------------------------------
1 | /*
2 | * The MIT License
3 | *
4 | * Copyright (c) 2010, InfraDNA, Inc.
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in
14 | * all copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | * THE SOFTWARE.
23 | */
24 | package com.infradna.tool.bridge_method_injector;
25 |
26 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
27 | import java.io.File;
28 | import java.io.IOException;
29 | import java.net.URL;
30 | import java.net.URLClassLoader;
31 | import org.apache.maven.plugin.AbstractMojo;
32 | import org.apache.maven.plugin.MojoExecutionException;
33 | import org.apache.maven.plugins.annotations.LifecyclePhase;
34 | import org.apache.maven.plugins.annotations.Mojo;
35 | import org.apache.maven.plugins.annotations.Parameter;
36 | import org.apache.maven.plugins.annotations.ResolutionScope;
37 | import org.jvnet.hudson.annotation_indexer.Index;
38 |
39 | /**
40 | * @author Kohsuke Kawaguchi
41 | */
42 | @Mojo(
43 | name = "process",
44 | requiresDependencyResolution = ResolutionScope.RUNTIME,
45 | defaultPhase = LifecyclePhase.PROCESS_CLASSES,
46 | threadSafe = true)
47 | public class ProcessMojo extends AbstractMojo {
48 | /**
49 | * The directory containing generated classes.
50 | */
51 | @Parameter(defaultValue = "${project.build.outputDirectory}", required = true)
52 | private File classesDirectory;
53 |
54 | @Override
55 | @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "user-provided value for running the program")
56 | public void execute() throws MojoExecutionException {
57 | try {
58 | for (String line : Index.listClassNames(
59 | WithBridgeMethods.class,
60 | new URLClassLoader(
61 | new URL[] {classesDirectory.toURI().toURL()},
62 | ClassLoader.getSystemClassLoader().getParent()))) {
63 | File classFile = new File(classesDirectory, line.replace('.', '/') + ".class");
64 | getLog().debug("Processing " + line);
65 | new MethodInjector().handle(classFile);
66 | }
67 | } catch (IOException e) {
68 | throw new MojoExecutionException("Failed to process @WithBridgeMethods", e);
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/injector/src/test/client/Main.java:
--------------------------------------------------------------------------------
1 | public class Main {
2 | public static void main(String[] args) throws Exception {
3 | // invocation of void method. verify that it runs without error
4 |
5 | Foo.toggle = false;
6 | Foo.hello();
7 | assertEquals(true, Foo.toggle);
8 |
9 | Foo.toggle = false;
10 | Foo.hello2();
11 | assertEquals(true, Foo.toggle);
12 |
13 | Foo.toggle = false;
14 | new Foo().hello3();
15 | assertEquals(true, Foo.toggle);
16 |
17 | Foo.toggle = false;
18 | new Foo().hello4();
19 | assertEquals(true, Foo.toggle);
20 |
21 | Foo.unbox();
22 | Foo.box();
23 |
24 | Object o = new Foo().getMessage();
25 | assertEquals(args[0],o);
26 |
27 | String n = new Foo().getString();
28 | assertEquals(args[0],n);
29 |
30 | Object s = Foo.getStaticMessage();
31 | assertEquals(args[0],s);
32 |
33 | Object w = Foo.methodToWiden(String.class);
34 | assertEquals(args[0],w);
35 |
36 | // using reflection to ensure that JIT isn't doing inlining
37 | check((Foo)Foo.class.newInstance(),args[0]);
38 | check((Bar)Bar.class.newInstance(),args[0]);
39 |
40 | Adapter a = new Adapter();
41 | assertEquals(1,a.i());
42 | assertEquals("http://kohsuke.org/",a.o());
43 | assertEquals("http://kohsuke.org/" + args[0], a.oParam(args[0]));
44 | assertEquals("http://kohsuke.org/" + args[0] + "/" + args[0] + "/" + args[0], a.oParams(args[0], args[0], args[0]));
45 |
46 | String[] array = a.array();
47 | assertEquals(1, array.length);
48 | assertEquals("http://kohsuke.org/", array[0]);
49 |
50 | new Adapter.SomeClass().someMethod();
51 |
52 | assertEquals(1,a.l());
53 | }
54 |
55 | private static void assertEquals(Object expected, Object actual) {
56 | System.out.println("We got "+actual+", expecting "+expected);
57 | if (!actual.equals(expected)) {
58 | System.exit(1);
59 | }
60 | }
61 |
62 | private static void check(IFoo f, String expected) {
63 | Object o = f.getMessage();
64 | assertEquals(expected,o);
65 |
66 | String n = f.getString();
67 | assertEquals(expected,n);
68 | }
69 |
70 | private static void check(IBar f, String expected) {
71 | Object o = f.narrow();
72 | assertEquals(expected,o);
73 |
74 | String n = f.widen();
75 | assertEquals(expected,n);
76 |
77 | String u = f.adapter();
78 | assertEquals("http://example.com/", u);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/injector/src/test/java/com/infradna/tool/bridge_method_injector/Junit4TestsRanTest.java:
--------------------------------------------------------------------------------
1 | package com.infradna.tool.bridge_method_injector;
2 |
3 | import org.junit.Test;
4 |
5 | public class Junit4TestsRanTest {
6 |
7 | @Test
8 | public void anything() {
9 | /*
10 | * Intentionally blank. We just want a test that runs with JUnit so that buildPlugin() works
11 | * in the Jenkinsfile.
12 | */
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/injector/src/test/synthetics/A.java:
--------------------------------------------------------------------------------
1 | public class A {
2 | public Object getProperty() {
3 | return null;
4 | }
5 | }
--------------------------------------------------------------------------------
/injector/src/test/synthetics/B.java:
--------------------------------------------------------------------------------
1 | import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
2 |
3 | public class B extends A {
4 |
5 | @WithBridgeMethods(value=String.class,castRequired=true)
6 | public CharSequence getProperty() {
7 | return null;
8 | }
9 | }
--------------------------------------------------------------------------------
/injector/src/test/v1/Adapter.java:
--------------------------------------------------------------------------------
1 | public class Adapter {
2 | public int i() { return 1; }
3 | public String o() { return "http://kohsuke.org/"; }
4 | public String[] array() {
5 | return new String[]{ "http://kohsuke.org/" };
6 | }
7 |
8 | public String oParam(String path) {
9 | return "http://kohsuke.org/" + path;
10 | }
11 |
12 | public String oParams(String path1, String path2, String path3) {
13 | return "http://kohsuke.org/" + path1 + "/" + path2 + "/" + path3;
14 | }
15 |
16 | // Just making sure we do not barf on Java 8 constructs:
17 | interface SomeInterface {
18 | default void someMethod() {}
19 | }
20 | static class SomeClass implements SomeInterface {
21 | @Override
22 | public void someMethod() {
23 | SomeInterface.super.someMethod();
24 | }
25 | }
26 |
27 | // we will evolve this from int -> long
28 | public int l() { return 1; }
29 | }
30 |
--------------------------------------------------------------------------------
/injector/src/test/v1/Bar.java:
--------------------------------------------------------------------------------
1 | public class Bar implements IBar {
2 | public String widen() { return "foo"; }
3 | public Object narrow() { return "foo"; }
4 | public String adapter() { return "http://example.com/"; }
5 | }
--------------------------------------------------------------------------------
/injector/src/test/v1/Foo.java:
--------------------------------------------------------------------------------
1 | public class Foo implements IFoo {
2 | /**
3 | * Testing narrowing. In v2, we'll narrow this to return the String type. This is type safe.
4 | */
5 | public Object getMessage() {
6 | return "foo";
7 | }
8 |
9 | /**
10 | * Testing widening. In v2, we'll widen this to Object. Potentially type unsafe.
11 | */
12 | public String getString() {
13 | return "foo";
14 | }
15 |
16 | public static Object getStaticMessage() {
17 | return "foo";
18 | }
19 |
20 | public static T methodToWiden(Class clazz) {
21 | return clazz.cast("foo");
22 | }
23 |
24 | static boolean toggle;
25 |
26 | public static void hello() {
27 | toggle = true;
28 | }
29 |
30 | public static void hello2() {
31 | toggle = true;
32 | }
33 |
34 | public void hello3() {
35 | toggle = true;
36 | }
37 |
38 | public void hello4() {
39 | toggle = true;
40 | }
41 |
42 | public static int unbox() {return Integer.MIN_VALUE;}
43 |
44 | public static int box() {return Integer.MAX_VALUE;}
45 | }
--------------------------------------------------------------------------------
/injector/src/test/v1/IBar.java:
--------------------------------------------------------------------------------
1 | public interface IBar {
2 | String widen();
3 | Object narrow();
4 | String adapter();
5 | }
--------------------------------------------------------------------------------
/injector/src/test/v1/IFoo.java:
--------------------------------------------------------------------------------
1 | public interface IFoo {
2 | public Object getMessage();
3 | public String getString();
4 | }
--------------------------------------------------------------------------------
/injector/src/test/v2/Adapter.java:
--------------------------------------------------------------------------------
1 | import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
2 |
3 | import java.io.IOException;
4 | import java.net.URL;
5 |
6 | public class Adapter {
7 | @WithBridgeMethods(value=int.class, adapterMethod="_i")
8 | public String i() { return "1"; }
9 |
10 | private Object _i(String o, Class type) {
11 | return Integer.parseInt(o);
12 | }
13 |
14 | @WithBridgeMethods(value=String.class, adapterMethod="_o")
15 | URL o() throws IOException { return new URL("http://kohsuke.org/"); }
16 |
17 | private Object _o(URL o, Class type) {
18 | return o.toString();
19 | }
20 |
21 | @WithBridgeMethods(value = String.class, adapterMethod="_o")
22 | public URL oParam(String path) throws IOException {
23 | return new URL("http://kohsuke.org/" + path);
24 | }
25 |
26 | @WithBridgeMethods(value = String.class, adapterMethod="_o")
27 | public URL oParams(String path1, String path2, String path3) throws IOException {
28 | return new URL("http://kohsuke.org/" + path1 + "/" + path2 + "/" + path3);
29 | }
30 |
31 | @WithBridgeMethods(value = String[].class, adapterMethod = "_array")
32 | URL[] array() throws IOException {
33 | return new URL[]{ new URL("http://kohsuke.org/") };
34 | }
35 |
36 | private Object _array(URL[] array, Class> type) {
37 | String[] result = new String[array.length];
38 | for (int i = 0; i < result.length; i++) {
39 | result[i] = array[i].toString();
40 | }
41 | return result;
42 | }
43 |
44 | interface SomeInterface {
45 | default void someMethod() {}
46 | }
47 | static class SomeClass implements SomeInterface {
48 | @Override
49 | public void someMethod() {
50 | SomeInterface.super.someMethod();
51 | }
52 | }
53 |
54 | @WithBridgeMethods(value=int.class, adapterMethod="l2i")
55 | public long l() { return 1L; }
56 |
57 | private Object l2i(long v, Class type) { return (int)v; }
58 | }
--------------------------------------------------------------------------------
/injector/src/test/v2/Bar.java:
--------------------------------------------------------------------------------
1 | import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
2 | import java.io.UncheckedIOException;
3 | import java.net.MalformedURLException;
4 | import java.net.URL;
5 |
6 | public class Bar implements IBar {
7 | @WithBridgeMethods(value = String.class, castRequired = true)
8 | public Object widen() { return "bar"; }
9 |
10 | @WithBridgeMethods(Object.class)
11 | public String narrow() { return "bar"; }
12 |
13 | @WithBridgeMethods(value = String.class, adapterMethod = "convert")
14 | public URL adapter() {
15 | try {
16 | return new URL("http://example.com/");
17 | } catch (MalformedURLException e) {
18 | throw new UncheckedIOException(e);
19 | }
20 | }
21 |
22 | private Object convert(URL url, Class> type) {
23 | return url.toString();
24 | }
25 | }
--------------------------------------------------------------------------------
/injector/src/test/v2/Foo.java:
--------------------------------------------------------------------------------
1 | import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
2 |
3 | public class Foo implements IFoo {
4 | @WithBridgeMethods(Object.class)
5 | public String getMessage() {
6 | return "bar";
7 | }
8 |
9 | /**
10 | * Widening String to Object.
11 | */
12 | @WithBridgeMethods(value=String.class,castRequired=true)
13 | public Object getString() {
14 | return "bar";
15 | }
16 |
17 | @WithBridgeMethods(Object.class)
18 | public static String getStaticMessage() {
19 | return "bar";
20 | }
21 |
22 | @WithBridgeMethods(value=String.class, castRequired=true)
23 | public static T methodToWiden(Class clazz) {
24 | return clazz.cast("bar");
25 | }
26 |
27 | static boolean toggle;
28 |
29 | @WithBridgeMethods(void.class)
30 | public static boolean hello() {
31 | toggle = true;
32 | return true;
33 | }
34 |
35 | @WithBridgeMethods(void.class)
36 | public static String hello2() {
37 | toggle = true;
38 | return "hello2";
39 | }
40 |
41 | @WithBridgeMethods(void.class)
42 | public boolean hello3() {
43 | toggle = true;
44 | return true;
45 | }
46 |
47 | @WithBridgeMethods(void.class)
48 | public String hello4() {
49 | toggle = true;
50 | return "hello4";
51 | }
52 |
53 | @WithBridgeMethods(value=int.class, castRequired=true)
54 | public static Integer unbox() {
55 | return Integer.MIN_VALUE;
56 | }
57 |
58 | @WithBridgeMethods(value=Integer.class)
59 | public static int box() {
60 | return Integer.MAX_VALUE;
61 | }
62 | }
--------------------------------------------------------------------------------
/injector/src/test/v2/IBar.java:
--------------------------------------------------------------------------------
1 | import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
2 | import java.net.URL;
3 |
4 | public interface IBar {
5 | @WithBridgeMethods(value=String.class,castRequired=true)
6 | Object widen();
7 | @WithBridgeMethods(Object.class)
8 | String narrow();
9 | @WithBridgeMethods(String.class)
10 | URL adapter();
11 | }
--------------------------------------------------------------------------------
/injector/src/test/v2/IFoo.java:
--------------------------------------------------------------------------------
1 | import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
2 |
3 | public interface IFoo {
4 | @WithBridgeMethods(Object.class)
5 | public String getMessage();
6 |
7 | @WithBridgeMethods(value=String.class,castRequired=true)
8 | public Object getString();
9 | }
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | org.jenkins-ci
6 | jenkins
7 | 1.132
8 |
9 |
10 |
11 | com.infradna.tool
12 | bridge-method-injector-parent
13 | ${revision}${changelist}
14 | pom
15 | Bridge Method Injection Parent POM
16 | Evolve your classes without breaking compatibility
17 | https://github.com/jenkinsci/bridge-method-injector
18 |
19 |
20 |
21 | MIT License
22 | https://opensource.org/licenses/MIT
23 | repository
24 |
25 |
26 |
27 |
28 |
29 | kohsuke
30 | Kohsuke Kawaguchi
31 |
32 |
33 |
34 |
35 | annotation
36 | injector
37 |
38 |
39 |
40 | scm:git:https://github.com/${gitHubRepo}.git
41 | scm:git:git@github.com:${gitHubRepo}.git
42 | ${scmTag}
43 | https://github.com/${gitHubRepo}
44 |
45 |
46 |
47 | 1.32
48 | -SNAPSHOT
49 | jenkinsci/bridge-method-injector
50 | false
51 |
52 |
53 |
54 |
55 | repo.jenkins-ci.org
56 | https://repo.jenkins-ci.org/public/
57 |
58 |
59 |
60 |
61 |
62 | repo.jenkins-ci.org
63 | https://repo.jenkins-ci.org/public/
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------