羡慕大劳星空顶?不如跟我一起使用 Jetpack compose 绘制一个星空背景(带流星动画)

news2024/12/29 23:25:18

前言

背景

作为一个自诩的电影爱好者,经常会在半夜看电影,看完后就会顺道去豆瓣标记一下看过,再看看别人对这个电影的理解。

某日深夜,看完电影后,顺手打开了豆瓣的 书影音记录 这个功能,起初并没有注意到这个页面的背景有什么东西,我以为只是一个普通的深色背景而已,直至一道流星突然划过屏幕!

好漂亮!我这才发现原来这个页面的背景是一个星空!时不时的还会有流星飞过!

这么漂亮的背景,不仿写一下真的对不起它了!

这个页面静态时是这样的:

s1.png

我把内容拉到最后,然后录制了一个动图,可以看到流星飞过的样子:

s2.gif

实现效果

这次依然使用 JetpackPack Compose 作为 UI 框架来实现。

最终实现效果如图:

p1.gif

代码地址

完整代码地址:starrySky

实现

分析背景组成

繁星

在开始实现之前,我们首先要分析一下豆瓣的这个背景都有些什么元素,它们的运行逻辑是什么。

我们先看一下这张仅有背景的截图:

s3.png

显而易见,该页面以纯黑色作为底色,然后点缀了一些白色或者说灰色的圆形小点,即繁星。

我原本以为这些繁星应该是随机生成的,但是经过我的观察和测试,实际上这些繁星都是固定不变的,我猜测这其实就是一整个静态图片。

但是我想实现不是这种的,如果只是一张静态图片那还有什么意思呢?

所以我准备更改为随机生成星星,且可以自定义星星的尺寸、颜色等参数。

流星

流星相对来说稍微复杂那么一点点,我做了一张流星局部放大且减速的动图:

s4.gif

从上面这个减速动图中可以看出,流星的生成有如下几个要点:

  1. 流星刚出现时有一个透明度逐渐减小的渐变效果
  2. 流星从出现到结束,一直都在沿着一条直线平移
  3. 流星刚出现时较短,并且逐渐变长,但是在达到一定长度后就不再变化

compose 自定义绘制基础知识

分析完这个页面由什么构成的后,我们先别急着直接开始写,我先扩展几个关于 compose 自定义绘制的基础知识,后面会用到。

DrawScope

首先,在compose中如果想要自己绘制的话,需要在 DrawScope 中才能使用我们在 view 中熟悉的 drawXXX 绘制相应的图形。

那么,怎么才能使用 DrawScope 呢?

我们可以直接使用 Canvans ,它的 onDraw 参数接收的就是一个作用域为 DrawScope 的匿名函数,我们可以在这个函数中进行我们的绘制操作,例如,这里我使用 drawRect 画了一个白色的矩形:

p2.png

不过,仔细想想,我们这里的需求,直接使用 Canvans 合适吗?

我们需要做的只是一个背景啊,直接使用 Canvans 虽然也能实现我们的需求,但是总觉得怪怪的。

不用担心,compose 还有一个地方也提供了 DrawScope ,那就是在 Modifier 中,在 Modifier 中自定义绘制的话特别适合于给已有的布局加东西。

而 Modifier 中有三个绘制相关的 API 可以使用,分别是 drawWithContentdrawBehinddrawWithCache

其中,drawWithContent 是和上面的 Canvans 差不多,并且可以通过更改 drawContent() 的位置,来实现控制绘制内容和这个控件原有内容的位置关系。

drawBehind 顾名思义就是把我们的内容放到原有内容之下,嗯?这不就是我们要的吗?绘制背景嘛。其实使用 drawWithContent 可以实现和这个 API 完全一致的效果,但是这里咱们直接使用这个就行。

drawWithCache 看名字就知道,是带有缓存的绘制,我们可以缓存住一些不需要改变的对象,避免重复创建对象的开销。

关于这三个 API 的使用可以参考 自定义绘制

给自定义绘制内容添加动画

知道了往哪儿绘制图形后,下一步是了解一下如何给自定义绘制内容添加动画效果。

其实,给绘制内容添加动画效果和给普通的 compose 控件加动画基本一致。

例如,我给上面这个矩形添加一个旋转动画可以这样写:

@Preview
@Composable
fun PreviewTest() {
    Column(
        Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        var state by remember {
            mutableStateOf(true)
        }
        
        val rotateValue by animateFloatAsState(targetValue = if (state) 90f else 0f)

        Canvas(
            modifier = Modifier.size(100.dp).clickable { state = !state }
            , onDraw = {
            withTransform(
                {
                    rotate(rotateValue)
                }
            ) {
                drawRect(Color.White)
            }
        })
    }
}

可以看到,与正常用法几乎没有区别,这里演示的是使用 draw 中的变换功能,旋转当前绘制的矩形,旋转的角度则由 animateFloatAsState 来提供,这样就实现了一个简单的旋转动画。

开始实现

基础结构

由于我们最终会在 Modifier 中进行绘制,如果直接写的话会显得很臃肿,而且也无法多次使用,所以我们需要实现一个 Modifier 的扩展函数,使用时只需要直接调用这个扩展函数即可:

fun Modifier.drawStarrySkyBg() : Modifier = composed { 
    drawBehind { 
        // ......
    }
}

使用时直接调用 Modifier.drawStarrySkyBg() 即可。

另外,在上面我们介绍过,可以使用 drawWithCache 缓存对象,为了性能更好,这里应该使用 drawWithCache 而不是直接使用 drawBehind

fun Modifier.drawStarrySkyBg() : Modifier = composed { 
    drawWithCache {
        // ……
        // 可以在这里初始化对象,这里的内容不会被 recompose
        
        onDrawBehind { 
            // ……
            // 这里和 drawBehind 一样,可以在这里进行绘制
        }
    }
}

绘制纯色背景

首先,我们直接绘制一个占满画布的矩形将背景覆盖掉,达到更改背景颜色的目的:

fun Modifier.drawStarrySkyBg(
    background: Color = Color.Black,
) : Modifier = composed {
    drawWithCache {
        // ……

        onDrawBehind {
            // ……
            // 绘制背景
            drawRect(color = background)
        }
    }
}

绘制星星

星星的绘制比较简单,直接使用 drawCircle 绘制圆形即可。

但是,这里我们需要实现的是,星星的位置、大小、颜色应该是随机的。

所以我们首先需要定义一个数据类 StarInfo 用于存放星星信息,然后在 CacheDrawScope 中初始化好星星信息,在 DrawScope 中直接根据这个信息绘制即可:

data class StarInfo(
    val offset: Offset,
    val color: Color,
    val radius: Float
)

当然,随机的颜色和尺寸应该是预设一组,而非真的完全随机,所以给这个函数添加参数

fun Modifier.drawStarrySkyBg(
    // ……
    starNum: Int = 20, // 需要生成多少个星星
    starColorList: List<Color> = listOf(Color(0x99CCCCCC), Color(0x99AAAAAA), Color(0x99777777)),
    starSizeList: List<Float> = listOf(0.8f, 0.9f, 1.2f),
    // ……
)

需要注意的是,这里的 starSizeList 并不是真正的圆形尺寸,而是缩放系数,因为圆形尺寸是按照当前可绘制区域的尺寸计算出来的,如果直接写死尺寸,会不太美观。

然后,定义并初始化星星信息:

drawWithCache {
    val random = Random(seed)
    val startInfoList = mutableListOf<StarInfo>()

    // 添加星星数据
    for (i in 0 until starNum) {
        val sizeScale = starSizeList.random(random)

        startInfoList.add(
            StarInfo(
                Offset( // 随机生成坐标
                    random.nextDouble(size.width.toDouble()).toFloat(), 
                    random.nextDouble(size.height.toDouble()).toFloat()
                ),
                starColorList.random(random),  // 随机选择一个预设颜色
                size.width / 200 * sizeScale  // 尺寸为可绘制区域大小的 1/200 并乘以随机选择到的缩放系数
            )
        )
    }
    
    // ……
}

上面代码中的 size 是当前可绘制区域的尺寸信息。

最后,开始绘制:

onDrawBehind {
    // ……
    
    // 绘制星星
    for (star in startInfoList) {
        drawCircle(color = star.color, center = star.offset, radius = star.radius)
    }
    
    // ……
}

绘制流星

绘制流星部分我们将分为三步走:

  1. 绘制出流星
  2. 让流星动起来
  3. 给流星加上一点细节

首先,我们需要绘制出流星的图案。

其实,这个流星无非就是一条直线,所以,我们只需要使用 drawLine 绘制直线即可。

drawLine 需要三个必须的参数:

  1. color: Color, 直线的颜色
  2. start: Offset, 直线的起点坐标
  3. end: Offset, 直线的终点坐标

为了提高扩展性,我们将颜色提出作为 drawStarrySkyBg 的参数,同时,流星并不是横平竖直的,而是有一定倾斜角度的,所以我们还要提供一个角度参数,另外,流星的线段宽度我们也提出来作为一个参数:

fun Modifier.drawStarrySkyBg(
    // ……
    meteorColor: Color = Color.White,
    meteorRadian: Double = 0.7853981633974483,  // 这里的角度是弧度,相当于45度
    meteorStrokeWidth: Float = 1f,
    // ……
) 

然后,绘制出一帧的流星:

drawLine(
    color = meteorColor,
    start = Offset(currentStartX, currentStartY),
    end = Offset(currentEndX, currentEndY),
    strokeWidth = meteorStrokeWidth
)

流星应该是从出现到结束一直都是在运动的,不可能是静态的,所以上面这个只是绘制出了流星某一个时刻的状态,所以我称之为绘制出了一帧。上面的起点坐标和终点坐标也应该是实时计算出来。

至于怎么计算的,我们先按下不表,先来说说怎么模拟流星的运动轨迹。

即,让流星动起来。

如果想要让绘制的内容动起来,理所当然的会想到应该使用动画相关的API,仔细分析一下我们这里的流星动画,它应该是无限运行的,因为流星需要一直都有,不能说是飞一次就销毁了是吧?

所以这里我们应该使用无限动画API rememberInfiniteTransition()

但是,应该将什么参数作为动画的值呢?

流星的坐标? 时间?

为了方便理解,这里我们选择使用时间作为动画值,而坐标由时间来实时计算出来。

因为如果直接将坐标作为动画值的话,不方便编写算法,同时也不好做出一些扩展。

编写动画参数如下:

val deltaMeteorAnim = rememberInfiniteTransition()
val meteorTimeAnim by deltaMeteorAnim.animateFloat(
    initialValue = 0f,
    targetValue = 300f,  // 这个值其实可以根据时间、速度、指定长度、以及当前绘制区域可用大小计算出来,但是我懒得算了,就直接写死一个比较大的值了
    animationSpec = infiniteRepeatable(
        animation = tween(durationMillis = meteorTime, delayMillis = meteorScaleTime, easing = LinearEasing)
    )
)

这里我们使用 meteorTimeAnim 作为模拟的时间值,需要注意的是这个值并不是和现实时间对应的,只是一个模拟变化值。

这个值将会无限的重复运行,每次运行都会间隔 meteorScaleTime 毫秒,并且单次运行持续时间为 meteorTime 毫秒。运行的内容是将 meteorTimeAnim 线性的从 0 过渡到 300。

上面提到的这几个参数都抽出来作为函数的参数:

fun Modifier.drawStarrySkyBg(
    // ……
    meteorTime: Int = 1500,
    meteorScaleTime: Int = 3000,
    // ……
) 

既然选择了时间作为变化的值,那么对于流星的运动,我们可以直接按照 时间x速度 来计算出它的运动路程,因此,再抽出一个参数作为速度:

fun Modifier.drawStarrySkyBg(
    // ……
    meteorVelocity: Float = 10f,
    // ……
) 

需要注意的是,这里速度也只是一个模拟值,并不是真正的速度。

有了时间和速度我们就可以计算出流星实时运行的坐标值了,对了,上面我们已经说了流星不是横平竖直的飞行的,而是有一个角度的,所以实际坐标值计算应该是:

val cosAngle = cos(meteorRadian).toFloat()
val sinAngle = sin(meteorRadian).toFloat()

// 计算当前起点坐标
currentStartX = startX + meteorVelocity * meteorTimeAnim * cosAngle
currentStartY = startY + meteorVelocity * meteorTimeAnim * sinAngle

其中,startXstartY 是我们随机生成的一个初始坐标,因为流星每次出现的初始位置应该是随机的而不是固定在一个地方,所以我们给他加了一个初始坐标。

当然,这个只是计算流星的起点坐标,对于终点坐标,我们则需要做一些处理。

还记得吗?上面我们分析的时候说过,流星的长度并不是一开始就是目标长度的,而是从 0 开始逐渐伸长到目标长度的。

所以我们需要在流星长度未达到目标长度时,让流星的终点坐标"跑"的比起点坐标快:

// 如果长度未达到目标长度,则开始增长长度,具体表现为计算终点坐标时,速度是起点的两倍
if (currentLength < meteorLength) {
    currentEndX = startX + meteorVelocity * 2 * meteorTimeAnim * cosAngle
    currentEndY = startY + meteorVelocity * 2 * meteorTimeAnim * sinAngle
}
else { // 已达到目标长度,直接用起点坐标加上目标长度即可得到终点坐标
    currentLength = meteorLength
    currentEndX = currentStartX + meteorLength * cosAngle
    currentEndY = currentStartY + meteorLength * sinAngle
}

在这里,我们直接把终点坐标运行的速度设置为起点坐标的两倍,其实这里可以编写一个更复杂的加速度算法,使得流星运行起来更自然,更舒适,但是这里我们就不写这么复杂了,感兴趣的可以自己修改。

其中,当前流星长度的计算公式为:

// 只有未达到目标长度才实时计算当前长度
if (currentLength != meteorLength) {
    currentLength = sqrt(
        (currentEndX - currentStartX).pow(2) + (currentEndY - currentStartY).pow(2)
    )
}

这就是数学中的计算两点之间的距离公式,这里就不展开讲了,感兴趣的可以自己去看看。

由于受到浮点数计算精度影响还有为了性能更优,我们只会在目标长度和当前实际长度不一致时才计算当前长度。

并且我们会在当前长度大于或等于目标长度时就直接把目标长度复制给当前长度,确保它俩能保持一致。

对了,流星的目标长度同样是抽出来作为函数的一个参数:

fun Modifier.drawStarrySkyBg(
    // ……
    meteorLength: Float = 500f,
    // ……
)

经过上面的计算,我们就能够得到一个飞翔的流星了。

接下来,就是给这个流星的动画加上一点细节。

首先是流星刚出来时的透明度过度动画:

val meteorAlphaAnima by deltaMeteorAnim.animateFloat(
    initialValue = 0f,
    targetValue = 1000f, // 透明度的动画时长应该是整体动画的 1/10 。这里直接使用1000作为目标值
    animationSpec = infiniteRepeatable(
        animation = tween(durationMillis = meteorTime, delayMillis = meteorScaleTime, easing = LinearEasing)
    )
)

// ……

// 绘制流星
drawLine(
    // ……
    alpha = (meteorAlphaAnima / 100).coerceAtMost(1f)
)

在这里,我们透明度的动画值依旧使用的是和时间一样的无限动画,只不过我们把目标值设置为了 1000, 然后在实际使用时将其除以 100 , 并且保证透明度不大于 1 (该参数不能大于1)。

这样处理的目的是使得透明度动画能够保持和时间的同步,并且确保透明度会在时间走了 1/10 时完全不透明,即只有最开始的 1/10 时间有透明度过渡效果。

其他的一些小细节,诸如流星已经飞出屏幕边界后就不再计算和绘制、流星初始坐标随机生成的边界控制、流星可以使用无限拖尾等这里就不再赘述,感兴趣的可以直接看代码。

代码非常简单,只有不到200行。

地址:starrySky

预览效果

这个函数封装好后使用十分简单,只需要在想要添加星空背景的组件的 modifier 参数加上 .drawStarrySkyBg() 即可,例如:

Column(
    Modifier
        .fillMaxSize()
        .drawStarrySkyBg(), // 给这个 Column 加上星空背景
    verticalArrangement = Arrangement.Center,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    var text by remember { mutableStateOf("Hello equationl  \n at starry sky\n${System.currentTimeMillis()}") }

    Text(
        text = text,
        color = Color.White,
        fontSize = 32.sp,
        modifier = Modifier.clickable {
            text = "Hello equationl  \n at starry sky\n${System.currentTimeMillis()}"
        }
    )
}

参考资料

  1. Exploring Jetpack Compose Canvas: the power of drawing
  2. Jetpack Compose 绘制 Canvas,DrawScope, 以及Modifier.drawWithContent,BlendMode讲解
  3. Custom Canvas Animations in Jetpack Compose
  4. Compose 自定义绘制

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/549307.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c++虚函数详解(多态特性)

1.c多态的概念 多态是c的特征之一 多态的分类&#xff1a;静态多态&#xff08;静态联编&#xff09;、动态多态&#xff08;动态联编&#xff09; 静态多态&#xff08;静态联编&#xff09;&#xff1a;函数入口地址 是在 编译阶段 确定&#xff08;运算符重载、函数重载&…

微信小程序自定义tabBar

1.app.json中tabBar里设置 "custom":true 设置好后就可使用自定义tabBar。 注意&#xff1a;list中的页面必须保存&#xff0c;且必须和自定义的tabBar页面数据一致 "tabBar": {"custom": true,"list": [{"pagePath": &q…

基于springboot篮球论坛系统

开发技术介绍 B/S架构 随着软件系统的不断改进和升级&#xff0c;B/S结构产品更为方便的特征体现地十分明显。对于一个中等偏大的公司来说&#xff0c;如果系统管理员每天要在很多台电脑之间来回查看&#xff0c;不断奔走&#xff0c;那么效率和工作量就会变得很低&#xff0…

【Redis】Redis实现点赞、点赞排行榜

目录 一、点赞 1、思路 2、代码实现 二、点赞排行榜 1、思路 2、代码实现 一、点赞 1、思路 在我们的项目中我们有时候会碰到这样的需求&#xff0c;比如实现一个博客系统&#xff0c;当用户访问到这篇博客时可以进行点赞&#xff0c;那么这个功能如何去实现呢&#xff…

Midjourney|文心一格 Prompt:完整参数列表、风格汇总、文生图词典合集

Midjourney|文心一格 Prompt&#xff1a;完整参数列表、风格汇总、文生图词典合集 1.Midjourney 完整参数列表 参数名称 调用方法使用案例注意事项V5V4V3niji版本在关键词后加空格&#xff0c;然后带上版本参数&#xff1a; --v 或者 —v –version 或者 —versionvibrant cali…

【密码学复习】第八讲 数字签名

数字签名&#xff08;Digital Signature&#xff09;&#xff0c;也称电子签名&#xff0c;是指附加在某一电子文档中的一组特定的符号或代码&#xff0c;它是利用数学方法对该电子文档进行关键信息提取并与用户私有信息进行混合运算而形成的&#xff0c;用于标识签发者的身份以…

SpringBoot学习之集成Swagger3(二十七)

一、Maven配置 注意swagger的版本号是3.0.0版本以上才可以,这里我们就选择3.0.0版本 完整的Maven配置如下(仅供参考): <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaL…

vulnhub渗透测试靶场练习1

靶场介绍 靶场名&#xff1a;Medium_socialnetwork 下载地址&#xff1a;https://www.vulnhub.com/entry/boredhackerblog-social-network,454/ 环境搭建 靶机建议选择VM VirtualBox,我一开始尝试使用VMware时会报错&#xff0c;所以改用VM VirtualBox&#xff0c;攻击机使用…

魔改车钥匙实现远程控车:(前传)在macOS上安装使用Arduino

前言 因为最近有个需求需要硬件支持&#xff0c;原本打算使用 Arduino Nano&#xff0c;后来在 Boot 大佬的建议下&#xff0c;买了某宇宙家的 ESP32C3 核心板&#xff0c;对比 Arduino Nano 价格便宜了一大半&#xff0c;而且自身就集成了 WIFI 和 BLE 模块&#xff0c;还不用…

(一)before initialization of D3D(初始化D3D之前你需要了解的D3D基础知识)

什么是D3D? D3D全称Direct X 3D,即一组API可以用来针对GPU编程,不过他最主要的作用是用来渲染(不过现在也有很多其他应用比如d3d11va[Direct X 3D 11 Video API]用来进行硬件加速解码) Tips:Direct X 3D主要用来渲染,既然我们说到可以针对GPU编程了,当然不只是渲染的工作可以…

国考省考行测:数量关系,牛吃草问题,比例问题

国考省考行测&#xff1a;数量关系&#xff0c;牛吃草问题 2022找工作是学历、能力和运气的超强结合体! 公务员特招重点就是专业技能&#xff0c;附带行测和申论&#xff0c;而常规国考省考最重要的还是申论和行测&#xff0c;所以大家认真准备吧&#xff0c;我讲一起屡屡申论…

〖Python网络爬虫实战㉘〗- Selenium案例实战(二)

订阅&#xff1a;新手可以订阅我的其他专栏。免费阶段订阅量1000 python项目实战 Python编程基础教程系列&#xff08;零基础小白搬砖逆袭) 说明&#xff1a;本专栏持续更新中&#xff0c;目前专栏免费订阅&#xff0c;在转为付费专栏前订阅本专栏的&#xff0c;可以免费订阅付…

图神经网络(处理点云)PPFNet的实现

文章说明&#xff1a; 1)参考资料&#xff1a;PYG官方文档。超链。 2)博主水平不高&#xff0c;如有错误还望批评指正。 3)我在百度网盘上传了这篇文章的jupyter notebook和有关文献。超链。提取码8848。 文章目录 前言文献阅读代码实操历史遗留问题 前言 本篇文章接上一篇文章…

今年的面试难度,我给跪了……

大家好&#xff0c;最近有不少小伙伴在后台留言&#xff0c;又得准备面试了&#xff0c;不知道从何下手&#xff01; 不论是跳槽涨薪&#xff0c;还是学习提升&#xff01;先给自己定一个小目标&#xff0c;然后再朝着目标去努力就完事儿了&#xff01; 为了帮大家节约时间&a…

【STM32G431RBTx】备战蓝桥杯嵌入式→决赛试题→第七届

文章目录 前言一、题目二、模块初始化三、代码实现interrupt.h:interrupt.h:main.h:main.h: 四、完成效果五、总结 前言 无 一、题目 二、模块初始化 1.LCD这里不用配置&#xff0c;直接使用提供的资源包就行 2.ADC:开启ADCsingle-ended 3.LED:开启PC8-15,PD2输出模式就行了…

手撕代码——同步FIFO

手撕代码——同步FIFO 一、FIFO原理与设计二、完整代码与仿真结果三、仿真结果 一、FIFO原理与设计 查看Xilinx官方FIFO IP核&#xff0c;其主要的信号有时钟信号、写端口信号、读端口信号&#xff0c;其中&#xff0c;写端口信号包括写满信号full、写使能信号wr_en、写数据输入…

[元带你学: eMMC完全解读 7] eMMC 设备与系统概述

依JEDEC eMMC 5.1及经验辛苦整理,付费内容,禁止转载。 所在专栏 《元带你学: eMMC完全解读》 前言 全文3600 字, 全文介绍eMMC 内部系统框架, Bus 总线宽度, 总线Speed Mode, 了解即可。对应Bus 总线重点看 8 Bit 即可, Speed Mode 重点看 HS400。几乎所有的系统都是跑在…

Java并发编程-synchronized

目录 1. synchronized在jdk 1.6中的优化 1.1 锁消除 1.2 锁粗化 1.2 锁升级/锁膨胀 1.2.1 锁升级原理 1.2.2 自适应自旋锁 2. synchronized实现原理 3. synchronized和Lock的对比 1. synchronized在jdk 1.6中的优化 在JDK1.5的时候,Doug Lee推出了ReentrantLock,lock的…

【密码学复习】第九讲 密钥管理(一)

密钥管理简介 • 柯克霍夫斯原则(Kerckhoffs Principle) 即使密码系统的任何细节已为人悉知&#xff0c;只要密钥未泄漏&#xff0c;它也应是安全的(19世纪). 密钥安全&#xff1a;三分技术&#xff0c;七分管理 密钥管理就是在授权各方之间实现密钥关系的建立和维护…

dom4j 读取xml配置文件,根据配置文件利用反射创建对象

pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4.0.0 …