Groovy Builder转换是否生效?私有字段使用遇方法缺失异常
问题场景
你在用Groovy的@Builder注解生成建造者模式时遇到了麻烦:实体类里的字段都是private final的,调用建造者的url()方法时抛出了MissingMethodException,但去掉private修饰符后代码就能正常运行。你希望保留字段的私有性,同时让建造者正常工作。
先来看下你的相关代码和报错:
项目配置
plugins { id 'groovy' id 'java' } group 'org.example' version '1.0-SNAPSHOT' sourceCompatibility = 11 repositories { mavenCentral() } dependencies { implementation 'org.codehaus.groovy:groovy-all:2.4.15' testCompile group: 'junit', name: 'junit', version: '4.12' testImplementation ('org.spockframework:spock-core:1.1-groovy-2.4') { exclude group: 'org.codehaus.groovy' } }
实体类代码
package pattern import groovy.transform.builder.Builder import groovy.transform.builder.DefaultStrategy @Builder class HttpRequestBuilderAnnotation { //Mandatory private final String url private final String httpMethod private final String requestContentType private final String acceptContentType //Optional private final String requestBody private final List<Integer> validResponseCodes String getUrl() { return url } String getHttpMethod() { return httpMethod } String getRequestContentType() { return requestContentType } String getAcceptContentType() { return acceptContentType } String getRequestBody() { return requestBody } List<Integer> getValidResponseCodes() { return validResponseCodes } }
测试代码
package pattern import spock.lang.Specification class HttpRequestBuilderAnnotationSpec extends Specification { private String url = 'https://dsd.com' void "Instantiate HttpRequest"() { when: HttpRequestBuilderAnnotation httpRequestBuilderAnnotation = HttpRequestBuilderAnnotation.builder() .url(url).build() then: assert httpRequestBuilderAnnotation assert httpRequestBuilderAnnotation.getUrl() == url } }
报错信息
groovy.lang.MissingMethodException: No signature of method: static pattern.HttpRequestBuilderAnnotation.url() is applicable for argument types: (java.lang.String) values: [https://dsd.com]
Possible solutions: use([Ljava.lang.Object;), grep(), dump(), any(), getUrl(), use(java.lang.Class, groovy.lang.Closure)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at pattern.HttpRequestBuilderAnnotationSpec.Instantiate HttpRequest(HttpRequestBuilderAnnotationSpec.groovy:12)
问题原因
核心问题在于@Builder默认使用的DefaultStrategy策略:它在编译时生成的建造者代码会尝试直接访问类的私有字段,但Java的访问权限规则不允许外部类(包括生成的内部建造者类)直接访问私有字段。虽然Groovy平时允许通过属性语法“绕过”私有访问,但@Builder是编译时生成代码,遵循严格的Java访问规则,所以就出现了找不到方法的错误。
解决方案
这里有两种靠谱的方案,都能保留字段的private final修饰符:
方案1:切换到ExternalStrategy策略
这个策略会生成一个独立的外部建造者类,通过调用类的构造方法来初始化实例,而不是直接访问私有字段。修改实体类的注解即可:
package pattern import groovy.transform.builder.Builder import groovy.transform.builder.ExternalStrategy @Builder(strategy = ExternalStrategy, prefix = "") class HttpRequestBuilderAnnotation { // 字段和getter方法完全保持不变 private final String url private final String httpMethod private final String requestContentType private final String acceptContentType private final String requestBody private final List<Integer> validResponseCodes String getUrl() { return url } String getHttpMethod() { return httpMethod } String getRequestContentType() { return requestContentType } String getAcceptContentType() { return acceptContentType } String getRequestBody() { return requestBody } List<Integer> getValidResponseCodes() { return validResponseCodes } }
这样修改后,你的测试代码不需要做任何改动,就能正常运行。
方案2:添加全参构造方法
如果不想切换策略,也可以给实体类添加一个包含所有字段的全参构造方法。@Builder会自动检测到这个构造方法,用它来生成建造者逻辑,避免直接访问私有字段:
package pattern import groovy.transform.builder.Builder import groovy.transform.builder.DefaultStrategy @Builder class HttpRequestBuilderAnnotation { private final String url private final String httpMethod private final String requestContentType private final String acceptContentType private final String requestBody private final List<Integer> validResponseCodes // 添加全参构造方法 HttpRequestBuilderAnnotation(String url, String httpMethod, String requestContentType, String acceptContentType, String requestBody, List<Integer> validResponseCodes) { this.url = url this.httpMethod = httpMethod this.requestContentType = requestContentType this.acceptContentType = acceptContentType this.requestBody = requestBody this.validResponseCodes = validResponseCodes } // getter方法保持不变 String getUrl() { return url } String getHttpMethod() { return httpMethod } String getRequestContentType() { return requestContentType } String getAcceptContentType() { return acceptContentType } String getRequestBody() { return requestBody } List<Integer> getValidResponseCodes() { return validResponseCodes } }
不过这个方案需要注意:因为你的字段都是必填的final字段,测试代码必须给所有必填字段赋值,否则会报错。比如修改测试代码:
void "Instantiate HttpRequest"() { when: HttpRequestBuilderAnnotation httpRequestBuilderAnnotation = HttpRequestBuilderAnnotation.builder() .url(url) .httpMethod("GET") .requestContentType("application/json") .acceptContentType("application/json") .build() then: assert httpRequestBuilderAnnotation assert httpRequestBuilderAnnotation.getUrl() == url }
推荐方案
优先选择方案1,因为它不需要修改构造方法,也不需要调整测试代码,完全符合建造者模式“分步构建”的初衷,同时完美保留了字段的私有性和不可变性。
内容的提问来源于stack exchange,提问作者Kaliyug Antagonist

