2 Commits
Author SHA1 Message Date
maxf 837aa3b49e 1.0.1
- 支持 lambda 作用域中使用 it 参数作为源 Bean
- 新增链式 Copy 复选框选项,默认不勾选
- 同时复制父类属性改为默认勾选
- 优化下拉框类名显示逻辑
- 移除目标类型解析校验,直接按源 Bean getter 拼接 set 方法
- 增加本地 intellijIdea 构建支持
2026-09-07 15:25:18 +08:00
maxf 4f32fa2b56 feat(BeanCopy): 重构 Bean 复制功能支持链式调用和 lambda 作用域
- 支持链式 Copy 模式:Target 填类名生成 new 类名().setXxx(...).setYyy(...)
- 支持 lambda 作用域:识别 lambda 参数如 it 作为源 Bean 进行复制
- 重构代码生成逻辑:分离普通复制和链式复制两种模式的实现
- 优化对话框界面:增加链式 Copy 选项,改进下拉框显示逻辑
- 更新变量收集范围:支持局部变量和 lambda 参数的混合选择
- 调整默认配置:父类属性复制选项默认勾选
- 优化编译配置:支持本地 IDEA 路径配置和 JDK 版本适配
2026-09-07 15:14:23 +08:00
8 changed files with 240 additions and 86 deletions

No files matched your search

+11
View File
@@ -2,6 +2,17 @@
本项目所有重要变更都记录在此文件中。
## [1.0.1] - 2026-09-07
### Added / Changed
- 支持 **lambda 作用域**:lambda 参数(如 `it`)也可作为源 Bean,可在 `map(it -> { ... it 复制给新对象 ... })` 中使用
- 新增「**链式 Copy**」复选框(默认不勾选):勾选后 Target 作为**类名**,生成 `new 类名().setXxx(src.getXxx()).setYyy(src.getYyy());`;未勾选时 Target 作为 **bean 名**,逐条生成 `bean.setXxx(src.getXxx());`
- 「**同时复制父类(继承)的属性**」改为**默认勾选**
- 下拉框类名显示优化:类名无重名时仅显示简单名;存在重名(不同包的同名类)时显示全限定名;类名+包名完全相同的重复项去重只保留一个;全限定名过长时自动截断中间
- 目标不再做类型解析/校验,直接按源 Bean 的 getter 属性拼出对应 set 方法
- 增加支持本地intellijIdea构建,避免下载依赖
## [1.0.0] - 2026-09-05
### Added
+5 -3
View File
@@ -27,7 +27,9 @@ b.setYyy(a.getYyy());
- 对话框的两个下拉框**可编辑**:既能下拉选择,也能直接输入变量名
- 若光标正位于某个变量上,打开对话框时会**自动预填**源/目标为那个变量
- 光标**选中**变量触发时,生成的代码会**替换选区**,去掉先前选中的变量名
- 提供「**同时复制父类(继承)的属性**」复选框(默认不勾选),勾选后连同父类/接口继承的 getter/setter 一起复制
- 支持 **lambda 作用域**:不仅识别局部变量,还能识别 lambda 参数(如 `it`),可在 `list.stream()...map(it -> { ... it 复制给新对象 ... }).toList()` 中直接把 `it` 当源 Bean
- 提供「**同时复制父类(继承)的属性**」复选框(**默认勾选**),勾选后连同父类/接口继承的 getter/setter 一起复制
- Target 填**类名**(经 IDE 自带类检索/索引解析)即可自动生成链式 `new Target().setXxx(src.getXxx()).setYyy(src.getYyy());`(只纳入返回目标类自身的链式 setter),适合 builder / 链式赋值场景;Target 填**变量名**则逐条生成赋值语句
## 环境要求
@@ -48,7 +50,7 @@ b.setYyy(a.getYyy());
或命令行(项目自带 wrapper,无需本机安装 Gradle):
```bash
./gradlew build # 产出 build/distributions/idea-plugin-copy-bean-1.0.0.zip(已让 build 依赖 buildPlugin)
./gradlew build # 产出 build/distributions/idea-plugin-copy-bean-1.0.1.zip(已让 build 依赖 buildPlugin)
./gradlew buildPlugin # 等价:仅产出插件 zip
```
@@ -69,7 +71,7 @@ b.setYyy(a.getYyy());
1. 执行 `./gradlew build` 或 `./gradlew buildPlugin`
2. 打开 IDEA → Settings → Plugins → ⚙ → Install Plugin from Disk…
3. 选择 `build/distributions/idea-plugin-copy-bean-1.0.0.zip` 并重启 IDE
3. 选择 `build/distributions/idea-plugin-copy-bean-1.0.1.zip` 并重启 IDE
## 项目结构
+21 -3
View File
@@ -5,9 +5,15 @@ plugins {
// 依赖仓库由 settings.gradle.kts 中的 dependencyResolutionManagement 统一配置
dependencies {
intellijPlatform {
// IntelliJ IDEA Community (IC),目标平台 2025.2+(build 252)
intellijIdea("2025.2.6.2")
// 把内置的 Java 插件(含 Java PSI)加入编译类路径,供本插件使用 PsiClass/PsiMethod 等
/* IntelliJ IDEA Community (IC),目标平台 2025.2+(build 252)
会在线下载巨大的包:https://download.jetbrains.com/idea/ideaIU-2025.2.6.2.win.zip (1.90 GB)
*/
// intellijIdea("2025.2.6.2")
/* 或者指向你电脑上已经安装好的 IDEA 路径,把内置的 Java 插件(含 Java PSI)加入编译类路径,供本插件使用 PsiClass/PsiMethod 等 */
val ideaPath = System.getenv("IDEA_HOME") // 使用 System.getenv() 读取环境变量 ①
?: providers.gradleProperty("localIdeaPath").orNull // 从 gradle.properties 中读取 ②(未配置时不抛异常)
?: "${System.getProperty("user.home")}/AppData/Local/Programs/IntelliJ IDEA Ultimate 2" // 兜底 ③
local(ideaPath)
bundledPlugin("com.intellij.java")
}
}
@@ -16,3 +22,15 @@ dependencies {
tasks.named("build") {
dependsOn("buildPlugin")
}
// 本机 JDK 为 Microsoft 版 21(无 JBR 的 Packages 布局),关闭字节码插桩。
// 仅影响 @NotNull 等注解的运行时检查,不影响本插件功能。
tasks.named("instrumentCode") {
enabled = false
}
// 指定支持的 IDEA 版本
tasks.withType<org.jetbrains.intellij.platform.gradle.tasks.PatchPluginXmlTask> {
sinceBuild.set("252") // 最小版本
// untilBuild.set("252.*") // 最大版本;如果你想让插件兼容所有后续版本,可以直接不设置
}
+4 -1
View File
@@ -1,5 +1,5 @@
group = top.yexuejc
version = 1.0.0
version = 1.0.1
org.gradle.jvmargs=-Xmx2048m
@@ -9,3 +9,6 @@ kotlin.stdlib.default.dependency = false
# 启用 Gradle 配置缓存与构建缓存
org.gradle.configuration-cache = true
org.gradle.caching = true
# 配置IDEA的安装路径(用于编译,也可以不设定)
#localIdeaPath=C:/Users/intasect/AppData/Local/Programs/IntelliJ IDEA Ultimate 2
@@ -12,8 +12,9 @@ import com.intellij.psi.util.PsiTreeUtil
/**
* 触发入口:Alt+Insert(Generate) -> Bean Copy... / 右键菜单 -> Bean Copy...
*
* 在方法作用域内选择源 Bean A 与目标 Bean B,然后遍历 A 的所有 getter,
* 在 B 中查找同名 setter,自动生成: b.setXxx(a.getXxx());
* 在方法(或 lambda)作用域内选择源 Bean,目标直接用字符串(不做类型解析/校验):
* - 未勾选「链式 Copy」:<target>.setXxx(src.getXxx()); ...(target 用作 bean 名)
* - 勾选「链式 Copy」:new <target>().setXxx(src.getXxx()).setYyy(src.getYyy());(target 用作类名)
*/
class BeanCopyAction : AnAction() {
@@ -34,11 +35,12 @@ class BeanCopyAction : AnAction() {
val offset = editor.caretModel.currentCaret.offset
val beanVars = collectBeanVariables(file, offset)
if (beanVars.size < 2) {
val dialogTitle = "Bean Copy"
if (beanVars.isEmpty()) {
Messages.showInfoMessage(
project,
"在当前作用域中未找到足够的 Bean 变量,请先写如:A a = new A(); B b = new B();",
"Bean Copy"
"在当前作用域中未找到可作为源 Bean 的变量,请先写如:A a = new A();",
dialogTitle
)
return
}
@@ -47,23 +49,48 @@ class BeanCopyAction : AnAction() {
val defaultName = findSelectedVariableName(file, offset)
val dialog = BeanCopyDialog(beanVars, project, defaultName)
if (!dialog.showAndGet()) return
val source = dialog.sourceVar
val target = dialog.targetVar
if (source == null || target == null) {
if (source == null) {
Messages.showInfoMessage(
project,
"源与目标 Bean 请选择有效变量(或输入作用域内存在、且带 getter/setter 的变量名)。",
"Bean Copy"
"源 Bean 请选择有效变量(或输入作用域内存在、且带 getter/setter 的变量名)。",
dialogTitle
)
return
}
val code = generateCopyCode(source, target, dialog.copyParent)
// 目标不做类型解析/校验:只依赖 Source 的属性拼接 set 方法。
val code: String?
if (dialog.chainCopy) {
val token = dialog.targetChainToken
if (token.isNullOrBlank()) {
Messages.showInfoMessage(
project,
"请在 Target 中填写一个类名(用于生成 new 类名().setXxx(...)),例如 TransactionModel。",
dialogTitle
)
return
}
code = generateChainCode(source, token, dialog.copyParent)
} else {
val name = dialog.targetBeanName
if (name.isNullOrBlank()) {
Messages.showInfoMessage(
project,
"请在 Target 中填写/选择一个目标变量名。",
dialogTitle
)
return
}
code = generateCopyCode(source, name, dialog.copyParent)
}
if (code == null) {
Messages.showInfoMessage(
project,
"源与目标 Bean 之间没有可复制的公共属性。",
"Bean Copy"
"源 Bean 没有可复制的属性。",
dialogTitle
)
return
}
@@ -81,40 +108,25 @@ class BeanCopyAction : AnAction() {
// ---------- 生成代码 ----------
private fun generateCopyCode(source: BeanVar, target: BeanVar, copyParent: Boolean): String? {
val sourceClass = source.cls
val targetClass = target.cls
val targetName = target.variable.name ?: return null
// 普通 Copy:按源属性逐一拼 <target>.setXxx(src.getXxx());
private fun generateCopyCode(source: BeanVar, targetName: String, copyParent: Boolean): String? {
// targetName 首字母小写
val tarName = targetName.replaceFirstChar { it.lowercase() }
val sourceName = source.variable.name ?: return null
// 是否把父类/接口继承的属性也纳入扫描
val sourceClasses = if (copyParent) classHierarchy(sourceClass) else listOf(sourceClass)
val targetClasses = if (copyParent) classHierarchy(targetClass) else listOf(targetClass)
val sourceClasses = if (copyParent) classHierarchy(source.cls) else listOf(source.cls)
val sb = StringBuilder()
var count = 0
val processed = HashSet<String>()
for (srcCls in sourceClasses) {
for (getter in srcCls.methods) {
// 只看 getter:0 个参数且符合 get/is 命名
if (!isGetter(getter)) continue
val property = PropertyUtil.getPropertyName(getter) ?: continue
// 子类已处理过的同名属性(父类覆盖场景)不再重复生成
if (!processed.add(property)) continue
// 在目标类(及其祖先)中手工查找同名 setter(避免依赖 findPropertySetter 的平台重载差异)
val setter = targetClasses.flatMap { it.methods.toList() }.firstOrNull {
isSetter(it) && PropertyUtil.getPropertyName(it) == property
} ?: continue
if (setter.parameterList.parametersCount != 1) continue
sb.append(targetName)
.append(".")
.append(setter.name)
.append("(")
.append(sourceName)
.append(".")
.append(getter.name)
.append("());\n")
val setterName = setterNameFor(getter) ?: continue
sb.append(tarName).append(".")
.append(setterName).append("(")
.append(sourceName).append(".").append(getter.name).append("());\n")
count++
}
}
@@ -122,6 +134,41 @@ class BeanCopyAction : AnAction() {
return sb.toString()
}
// 链式 Copy:new <target>().setXxx(src.getXxx()).setYyy(src.getYyy());
private fun generateChainCode(source: BeanVar, targetToken: String, copyParent: Boolean): String? {
val sourceName = source.variable.name ?: return null
val sourceClasses = if (copyParent) classHierarchy(source.cls) else listOf(source.cls)
val sb = StringBuilder("new ").append(targetToken).append("()")
var count = 0
val processed = HashSet<String>()
for (srcCls in sourceClasses) {
for (getter in srcCls.methods) {
if (!isGetter(getter)) continue
val property = PropertyUtil.getPropertyName(getter) ?: continue
if (!processed.add(property)) continue
val setterName = setterNameFor(getter) ?: continue
sb.append("\n .").append(setterName)
.append("(").append(sourceName).append(".").append(getter.name).append("())")
count++
}
}
if (count == 0) return null
sb.append(";")
return sb.toString()
}
// 由 getter 反推 setter 方法名:getXxx -> setXxx;isXxx -> setXxx
private fun setterNameFor(getter: PsiMethod): String? {
val name = getter.name
val prop = when {
name.startsWith("get") && name.length > 3 -> name.substring(3)
name.startsWith("is") && name.length > 2 -> name.substring(2)
else -> return null
}
return "set" + prop.replaceFirstChar { it.titlecase() }
}
// 收集类 + 所有父类/接口,用于扫描继承到的属性(子类优先,自动去重)
private fun classHierarchy(cls: PsiClass): List<PsiClass> {
val result = LinkedHashSet<PsiClass>()
@@ -142,18 +189,15 @@ class BeanCopyAction : AnAction() {
return PropertyUtil.getPropertyName(m) != null
}
private fun isSetter(m: PsiMethod): Boolean {
if (m.parameterList.parametersCount != 1) return false
if (m.hasModifierProperty(PsiModifier.STATIC)) return false
return PropertyUtil.getPropertyName(m) != null
}
// ---------- 收集作用域内的 Bean 变量 ----------
private fun collectBeanVariables(file: PsiFile, offset: Int): List<BeanVar> {
val element = file.findElementAt(offset) ?: return emptyList()
val scope = findScope(element) ?: return emptyList()
val vars = PsiTreeUtil.collectElementsOfType(scope, PsiLocalVariable::class.java)
// 局部变量 + lambda 参数(lambda 的 it / 命名参数是 PsiParameter,而非 PsiLocalVariable)
val locals = PsiTreeUtil.collectElementsOfType(scope, PsiLocalVariable::class.java)
val params = PsiTreeUtil.collectElementsOfType(scope, PsiParameter::class.java)
val vars = locals + params
return vars.mapNotNull { v ->
val cls = resolveBeanClass(v.type) ?: return@mapNotNull null
if (!hasGetterOrSetter(cls)) return@mapNotNull null
@@ -163,12 +207,8 @@ class BeanCopyAction : AnAction() {
private fun findScope(element: PsiElement): PsiElement? {
val method = PsiTreeUtil.getParentOfType(element, PsiMethod::class.java)
if (method != null && method.body != null) return method.body
val block = PsiTreeUtil.getParentOfType(element, PsiCodeBlock::class.java)
if (block != null) return block
val field = PsiTreeUtil.getParentOfType(element, PsiField::class.java)
if (field != null) return field
return null
val scope = method?.body ?: PsiTreeUtil.getParentOfType(element, PsiCodeBlock::class.java)
return scope ?: PsiTreeUtil.getParentOfType(element, PsiField::class.java)
}
private fun resolveBeanClass(type: PsiType): PsiClass? {
@@ -177,13 +217,16 @@ class BeanCopyAction : AnAction() {
}
private fun hasGetterOrSetter(cls: PsiClass): Boolean =
cls.methods.any { isGetter(it) || isSetter(it) }
cls.methods.any { isGetter(it) || isSetterMethod(it) }
// 返回光标位置所在的局部变量名(用于预填源/目标下拉框)
private fun isSetterMethod(m: PsiMethod): Boolean =
m.parameterList.parametersCount == 1 && !m.hasModifierProperty(PsiModifier.STATIC) && PropertyUtil.getPropertyName(m) != null
// 返回光标位置所在的变量名 / lambda 参数名(用于预填源/目标下拉框)
private fun findSelectedVariableName(file: PsiFile, offset: Int): String? {
val element = file.findElementAt(offset) ?: return null
val refVariable = file.findReferenceAt(offset)?.resolve() as? PsiLocalVariable
val variable = refVariable ?: PsiTreeUtil.getParentOfType(element, PsiLocalVariable::class.java)
val refVariable = file.findReferenceAt(offset)?.resolve() as? PsiVariable
val variable = refVariable ?: PsiTreeUtil.getParentOfType(element, PsiVariable::class.java)
return variable?.name
}
}
}
@@ -9,11 +9,14 @@ import javax.swing.JComponent
import javax.swing.JPanel
/**
* 选择源 Bean(读取属性)与目标 Bean(写入属性)的对话框。
* 选择源 Bean(读取属性)与目标(写入属性)的对话框。
*
* 两个下拉框均可编辑:既可从列表中选择,也可直接输入变量名。
* 若传入 [defaultName],打开时会把源/目标都预填为该变量。
* 可通过 [copyParent] 决定是否连父类/接口继承的属性一起复制。
* - Source / Target 下拉框均可编辑:既可从列表中选择,也可直接输入。
* - Target 不做类型解析/校验:
* - 未勾选「链式 Copy」时,Target 的字符串当作 **bean 名** -> 生成 `m1.setXxx(src.getXxx());`
* - 勾选「链式 Copy」时,Target 的字符串当作 **类名** -> 生成 `new 类名().setXxx(src.getXxx()).setYyy(...)`
* - 下拉框展示:类名无重名时仅显示简单名;有重名时显示全限定名;类名+包名重复项去重;超长截断。
* - [copyParent] 默认勾选,复制时纳入父类/接口继承的属性。
*/
class BeanCopyDialog(
private val vars: List<BeanVar>,
@@ -21,48 +24,117 @@ class BeanCopyDialog(
defaultName: String? = null,
) : DialogWrapper(project, true) {
private val sourceBox = JComboBox(vars.toTypedArray())
private val targetBox = JComboBox(vars.toTypedArray())
// 去重 + 计算显示文本后的列表(下拉框项与解析都基于它)
private val comboVars: List<BeanVar> = prepareDisplayVars()
private val sourceBox = JComboBox(comboVars.toTypedArray())
private val targetBox = JComboBox(comboVars.toTypedArray())
private val chainCheck = JCheckBox("链式 Copy:将 Target 作为类名,生成 new 类名().setXxx(...).setYyy(...)")
private val copyParentCheck = JCheckBox("同时复制父类(继承)的属性")
val sourceVar: BeanVar? get() = resolveSelection(sourceBox)
val targetVar: BeanVar? get() = resolveSelection(targetBox)
/** 是否连父类/接口继承的属性一起复制(默认不选中) */
/** 是否生成链式 new 类名().set...()(默认不勾选) */
val chainCopy: Boolean get() = chainCheck.isSelected
/** 是否连父类/接口继承的属性一起复制(默认勾选) */
val copyParent: Boolean get() = copyParentCheck.isSelected
/** 普通模式:Target 的 bean 名(选变量 -> 变量名;输入 -> 字符串原样) */
val targetBeanName: String? get() = targetText { it.variable.name }
/** 链式模式:Target 的类名 token(选变量 -> 其类型(类简单名);输入 -> 字符串原样) */
val targetChainToken: String? get() = targetText { it.cls.name }
init {
title = "Bean Copy"
sourceBox.isEditable = true
targetBox.isEditable = true
copyParentCheck.isSelected = true
prefill(defaultName)
init()
}
// 默认把源/目标都设为指定变量名对应的 BeanVar(若存在于列表中)
/** 默认把源/目标都设为指定变量名对应的 BeanVar(若存在于列表中) */
private fun prefill(name: String?) {
if (name.isNullOrBlank()) return
val matched = vars.firstOrNull { it.variable.name == name } ?: return
val matched = comboVars.firstOrNull { it.variable.name == name } ?: return
sourceBox.selectedItem = matched
targetBox.selectedItem = matched
}
// 解析下拉框当前值:选中的对象或直接输入的名称都能映射到 BeanVar
/** 解析源下拉框当前值:选中的对象或直接输入的名称都能映射到 BeanVar */
private fun resolveSelection(box: JComboBox<*>): BeanVar? {
val sel = box.selectedItem
return when (sel) {
is BeanVar -> sel
is String -> vars.firstOrNull { it.variable.name == sel }
is String -> comboVars.firstOrNull { it.variable.name == sel }
else -> null
}
}
/** 取 Target 文本:选中的变量按需取变量名/类名;输入的原样返回(无校验) */
private fun targetText(selector: (BeanVar) -> String?): String? {
val sel = targetBox.selectedItem
return when (sel) {
is BeanVar -> selector(sel)
is String -> sel.trim().ifEmpty { null }
else -> null
}
}
/**
* 生成下拉框项:
* 1) 类名+包名都相同(同一类)的重复项只保留一个;
* 2) 类简单名在本作用域内出现多次(不同包的同名类)时,这些项显示全限定名,其余仅显示简单名。
*/
private fun prepareDisplayVars(): List<BeanVar> {
// 按类的唯一标识去重(qualifiedName 相同视为同一类)
val deduped = LinkedHashMap<String, BeanVar>()
for (v in vars) {
val key = v.cls.qualifiedName ?: v.cls.name ?: continue
deduped.putIfAbsent(key, v)
}
val list = deduped.values.toList()
// 统计类简单名的出现次数,判断哪些需要显示全限定名
val simpleNameCount = HashMap<String, Int>()
for (v in list) {
val s = v.cls.name ?: continue
simpleNameCount[s] = (simpleNameCount[s] ?: 0) + 1
}
for (v in list) {
val simple = v.cls.name ?: continue
val varName = v.variable.name ?: "?"
v.display = if (simpleNameCount[simple]!! > 1) {
val fq = v.cls.qualifiedName ?: simple
"$varName : ${truncateFq(fq)}"
} else {
"$varName : $simple"
}
}
return list
}
/** 全限定名过长时截断中间(保留首尾段与类名),避免下拉框被撑宽 */
private fun truncateFq(fq: String, max: Int = 56): String {
if (fq.length <= max) return fq
val simple = fq.substringAfterLast('.')
val pkg = fq.substringBeforeLast('.')
val budget = max - simple.length - 1
if (budget <= 0) return simple
val keep = budget / 2
return "${pkg.take(keep)}…${pkg.takeLast(keep)}.$simple"
}
override fun createCenterPanel(): JComponent {
return FormBuilder.createFormBuilder()
.addLabeledComponent("Source Bean (读取属性):", sourceBox)
.addLabeledComponent("Target Bean (写入属性):", targetBox)
.addComponent(chainCheck)
.addComponent(copyParentCheck)
.addComponentFillVertically(JPanel(), 0)
.panel
}
}
}
@@ -1,14 +1,20 @@
package top.yexuejc.beancopy
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiLocalVariable
import com.intellij.psi.PsiVariable
/**
* 作用域内的一个 Bean 变量(局部变量 + 其解析出的类)。
* 作用域内的一个 Bean 变量(局部变量或 lambda 参数 + 其解析出的类)。
*
* 在对话框的下拉框中以下列文本显示:
* <code>varName : ClassSimpleName</code>
* 下拉框中显示的文本由 [display] 决定:
* - 类名在本作用域内**无重名**时,显示 <code>varName : ClassSimpleName</code>
* - 类名**有重名**时,显示 <code>varName : 全限定名</code>(用于区分不同包下的同名类)
*/
class BeanVar(val variable: PsiLocalVariable, val cls: PsiClass) {
override fun toString(): String = "${variable.name} : ${cls.name}"
}
class BeanVar(val variable: PsiVariable, val cls: PsiClass) {
/** 下拉框显示文本;由 [top.yexuejc.beancopy.BeanCopyDialog] 按重名/去重情况计算后赋值 */
var display: String? = null
override fun toString(): String =
display ?: "${variable.name} : ${cls.name}"
}
+4 -5
View File
@@ -7,17 +7,16 @@
<b>Bean Copy</b> 一键把源 JavaBean 的属性值通过 get/set 方法复制到目标 JavaBean 上。
<br>
用法:在方法体内写 <code>A a = new A(); B b = new B();</code>,将光标置于方法内,<br>
按 <b>Alt+Insert</b> (Generate) 选择 <b>Bean Copy...</b>,在弹出的对话框中<br>
按 <b>Alt+Insert</b> (Generate) 或者<b>右键</b>选择 <b>Bean Copy...</b>,在弹出的对话框中<br>
选择源 Bean 与目标 Bean,即可自动生成:<br>
<code>b.setXxx(a.getXxx());</code> 等所有可复制的赋值语句。
<code>b.setXxx(a.getXxx());</code> 等所有可复制的赋值语句。<br>
支持 lambda 作用域(把 lambda 的 <code>it</code> 参数当源 Bean);Target 填写<br>
类名(经 IDE 类检索)即自动生成链式 <code>new Target().setXxx(...).setYyy(...);</code>。
]]></description>
<depends>com.intellij.modules.platform</depends>
<depends>com.intellij.modules.java</depends>
<!-- 支持 2025.2+ 平台(build 252) -->
<idea-version since-build="252" />
<extensions defaultExtensionNs="com.intellij">
</extensions>