几万年前,孙悟空的一次删库跑路...

话说几万年前,有一只猴子在大闹地府删库跑路,导致地府几百年没缓过劲儿来......

在知乎上冒出这么一个问题:“孙悟空无姓无名的时候,阎王生死簿是怎么写的呢?”

生死薄技术上如何实现?广大生灵在生死薄中的唯一标记是什么?阴间数据库是什么样一个数据库?于是脑洞大开的程序员开始了他们的表演.....

地狱数据库系统到底是什么样的?

来自知乎网友大海的回复:

https://www.zhihu.com/question/29775354/answer/45744415

这个问题让我对地狱数据库系统(Hell-DBMS )进行了几点小思考,开个脑洞。

首先,地狱必须有数据库,数据量太大了。

每个生灵都要有记录,且必须是实时记录,要进行数据分析。想象一下各种生灵,万物有灵,大大小小,连蝼蚁飞蛾也是命,从单细胞到现代社会的数据应该有多大。

数据库的话每个生灵就要有唯一标记。实名反对说是名字主键的,这是基本知识,名字重名怎么办,数据库原理请重修。

实名反对说是 IP 地址标记的,IPV4 很快就用光的好不好;IPV6 貌似在生物历史长河中也是不够的,朝生暮死都是生灵,这么多年过去了,这数据积累。

把自动生成的唯一 ID 当主键相对还靠谱,但位数必须相当长,数据库得特别设计,如此大数据至少要谷歌技术支持,也许叫地狱歌,SQL-SERVER 之类的技术根本顶不住。

搞 Hell-DBMS 请先看下大技术:

  • Hoogle File System

  • Hoogle Bigtable

  • Hoogle MapReduce

对了,《开源海量数据库技术在阴间生死管理系统中的研究与实践》应该获得天庭科技进步特等奖的。

其次,查询效率必须高。

查询效率低的话,阎王还得点支烟等半天结果么,经常有上级官员过来查数据,玉皇啊,如来啊,即使是阿难、迦叶来也是惹不起的人,用户不满意,KPI 不行、绩效差阎王官位不稳的。

业务量这么大,每天至少插入数亿条新记录,删除数亿条记录,所有善恶状态数据都要实时记录,想想要接多少善恶传感器,信道冲突肯定很难解决。

好事坏事用 Wi-Fi 还是 Zigbee 传的不清楚,说不定某米会推出家庭善恶智能数据处理中心。

生灵死掉之后还要迅速进行大数据分析,判定死人到底应该进几层地狱。数据分析慢了奈何桥都要排队,用户差评有木有!

数据粒度非常非常细,死亡时间三更五更都不能差,下了地狱打多少下铁棍都要精确计算,况且还会有许多异常发生,有时候要回滚,有可能不小心操作错了(死而复生应该就是地府回滚,详细请见《聊斋志异》[1])。

有时候要灾难恢复,比如孙猴子捣乱引起的灾难性数据损失;比如用户投诉问题,凭什么猴子要短命?

这种问题只有孙猴子问得出,不仅问得出还直接上门责问,地府的安保工作真的要加强。

对了,像悟空这种异常用户,Sa 恨不得一删了之有木有?(不懂 Sa 的 IT 人士请自行面壁,Admin 也算 Sa)

再次,必须能应对瞬时并发高峰数据。

战争来了,成千上万的人阵亡;瘟疫了,成千上万人逝去;灭鼠了,几万几十万老鼠完蛋;飞机撒农药了,多少修行不够的小精灵批量完蛋。

有生灵死亡必须要登记并把流程向前推进,这是典型的移动应用,无数的勾魂小鬼在短时间内飞速赶到现场。

管它是扫二维码还是近场通讯技术 NFC,反正无数小鬼同时用移动客户端向 Hell-DBMS 系统上传数据,App 必须友好,后台必须能顶住。

不能学 12307-1 总是掉链子,12307-1 掉了链子还能骂它:“去死!Go to Hell!”,Hell-DBMS 可怎么骂才好。

所以呢:关键时刻,服务器不能卡住,数据库性能不能下降,生死薄必须实时更新。

最后,必须有大数据分析和预测技术。

阴间有谛听,可以通过“听”,得到过去数据和未来的数据,这明显是大数据和云结合的杰作。

为什么是听呢?因为阴间数据库已经把数据语音化了,用定向波束直接送到谛听耳边,电磁监听根本没有效果,几乎不可能泄密。定向波束的技术在加大功率后可同时用于在阴间跳广场舞的某些亡灵们。

那么孙猴子在阴间里删除数据,怎么删除的?后来如何?

结论 1: 猴子要删除数据,应该是从界面删除的,没什么高技术,纯粹的社会工程而已。

巨型数据库,大数据数据库一定是分解得比较细的,删除的话至少是多表级联删除,直接从主表 DELETE 未免要引发异常。

再说孙猴子不是计算机专业的,应该是用金箍棒顶着小鬼的顶梁门,胁迫他用超级用户进去,选择界面的删除功能搞定的。

我猜阴间数据库删除要左右各一个小鬼,持阎王发的优盾,同时输入口令。

孙猴子反正克隆能力强,变出几个分身分别逼住就能搞定。这个 Bug 系统必须要改进。

所以这是正常删除,删除之后,轮回系统并没有完全混乱,只影响了一部分数据。

即使给孙猴子开个 CONSOLE,他也记不住命令,猴急猴急,抓耳挠腮,他最多会点点鼠标。

结论 2:阴间数据库有强劲的灾难恢复功能。

话说,猴子完全低估了程序员们的实力。海量数据库都有强大的异地容灾备份功能,数据应该是备在最安全的雷音寺(第三方),所有操作均有 LOG。

在西方以如来为首的专家团指导下(具体操作应该是负责安全保卫的天王,成就归于领导),数据迅速恢复,猴子们根本没有得到永生,在西方如来团队的支持下数据迅速恢复,猴子家族应该死还是死。

孙猴子自己么,虽然罪过不小,但是他会闹,能力还强。为了和谐天庭管理层还是为他做了特殊标记,在数据库里加上一个 TAG,设定为神仙级,计算寿命但不设定界限,有异常情况直接发出系统警报,和 RuLai -SkyNet All-in System 系统联动,确保一方平安。

参考文献:

0 、《Big Data Application Platform for Hell》[J] InHell  Hell-SCI收录
1、《论Paxos算法在阴间生死管理系统中的应用与优化》[J] 阴间信息技术 玉帝元年 第7788卷 核刊
2、《论Consistent Hash在阴间生死管理系统云中的应用》[M]阴间信息技术 玉帝9527年 第125222 核刊
3、 《论超大规模稠密矩阵在阴间生死管理系统中的理论研究》[C] 信息技术阴间应用大会 9528
4、 《论孟婆汤在阴间生死管理系统库存管理当中的管理流程》 [J] 阴间食品与营养 VOL 2241554
5、《论牛头马面阴间勾人大队的管理电子化》[J] 阴间数字化城管研究 VOL15486488789
6、《论天庭-西天-阴间点对点技术在阴间办公自动化中的实现》 [J] 阴间实用软件增刊。

地狱数据库是如何设计的?

来自知乎网友萝魏紫的回复:

https://www.zhihu.com/question/29775354/answer/287551487

关于孙悟空无姓无名的时候,阎王生死簿是怎么写的呢?这个问题,当然是 ID 呀,每个东西 New 出来就有个 ID,没人用 Name 做主键的!

根据原文可以得知:

悟空道:“胡说!胡说!常言道:‘官差吏差,来人不差。’你快取生死簿子来我看!”十王闻言,即请上殿查看。 

悟空执着如意棒,径登森罗殿上,正中间南面坐上。十王即命掌案的判官取出文簿来查。

那判官不敢怠慢,便到司房里,捧出五六簿文书并十类簿子,逐一查看。裸虫、毛虫、羽虫、昆虫、鳞介之属,俱无他名。

又看到猴属之类,原来这猴似人相,不入人名;似裸虫,不居国界;似走兽,不伏麒麟管;似飞禽,不受凤凰辖。

另有个簿子,悟空亲自检阅,直到那魂字一千三百五十号上,方注着孙悟空名字,乃天产石猴,该寿三百四十二岁,善终。

悟空道:“我也不记寿数几何,且只消了名字便罢!取笔过来!”那判官慌忙捧笔,饱掭浓墨。悟空拿过簿子,把猴属之类,但有名者,一概勾之!

阎王们只有硬 Copy,但是在硬 Copy 上更改,也会生效,所以应该是每天晚上跑 Batch 同步。

你看,原文有告诉你数据库设计了,首先他是分类型的,我估计可能是按照比如生物学那种树状分类,所以我们可以认为,生死簿应该是树状的 NoSQL 存储,或者实现了树状表,子表的 RMDB。

你仔细看,孙悟空属于魂字1350 号,这个魂字,一定是 Namespace 了,然后是自增主键,主键上标有自然信息,名字,类型,年龄,所以,这个主键是记录创建的时候给的,名字确定了,再补而已。

而且,你看孙悟空和其他猴子不在一个猴属之中,更确定了生死薄是树状的存储结构。

因为主键记录上有死亡时间,看起来是每天晚上跑个 Batch,把当前时间-出生时间=死亡时间的数据筛选出来,送去执行部门干掉。触发器太麻烦,跑 Batch 拉个报表给黑白无常就可以了。

这个系统有问题,更新的 Batch 不看数据是否有篡改就直接更新,这说明数据安全性没有考虑,我给地府推荐 OWASP 项目,用来提高安全性。

我曾和几个架构师聊天聊到这个问题,大家觉得这个主意很有趣,发起了《我帮阎王设计表》主题活动,来锻(qiong)炼(ji)设(wu)计(liao)能(xia)力(che)!

我汇总了下 ER 高层设计,如下图:

主要来说,首先有一个字典表,规定了生物分类(CATE),考虑到每种分类的 UUID 类型应该不同。

比如孙悟空属于的魂字,看上去东西就不多,很可能就是一个 int id,但是如果是虫子类,东西可能太多,一个 long 都不一定能装下,可能需要带编码的 vchar。

所以给每个 CATE ID 定义一种自增编码方式,以兼容将来万一出现机器人也要死,这样地府的系统不需要重做。

给予每种 ID 一个表明后缀,这样可以分表,不用把每种都放在同一个表里。

对于 Transaction 表,每种属性都有两个表,一个是已死表,作为历史数据备查;一个是存活表,这样做到了读写分离,增强性能。

每天新增的生物,根据其自己的 UID 插入表,主键写入速度有保证,这点上,考虑到地府不负责出生,我们提供一个 AMQP 高性能 Message Q 来给出生部门,可能是送子观音来写入,当然也可以提供 Restful API。

同时,每天晚上跑个 Batch,遍历存活表,将死期是今天的数据筛选出来,放入 Dead 表,同时生成报表,交给索命部门,也就是黑白无常做实际杀死工作。

所以架构图也出来了:

看到这里,我不得不说,程序员们是真的皮....难道不怕被阎王喊去面向地狱编程?

真的有程序员做出了完整的地府后台管理系统

这不,前段时间,就有这么一个段子火了。某位程序员日有所思,夜有所梦,终于有一天梦见自己见阎王爷了。阎王爷还叫他给生死簿做个后台管理系统。

还真有程序员把地府后台管理系统原型做出来了,目前这个项目已经在开发中...

Github围观地址为:

https://github.com/canxin0523/thesixsectorTeam

小编看了下 Demo,功能相当齐全:

http://kzgfmo.axshare.cn

用户登录:为了避免几百年前被猴子删库这样的悲剧再度重演,有编制的地府工作人员必须使用账号密码登录才能访问管理系统。

用户权限:不同等级的员工,所拥有的权限也应该各不相同,各司其职。

数据看板:作为老板,阎王每天要做的,就是喝喝茶、看看折线图和数字就行了。

生死簿管理:这也是地府最重要的数据资产——地府职工需要依据生死簿的记载,依次进行勾魂、地狱刑罚、投胎轮回等等业务流程。就算是被删除的数据,也都会被记录在案,以供随时恢复。

勾魂管理:如果黑白无常和牛头马面勾错了魂,也不是没有挽救的机会。

审判记录:被勾来的魂到地府报道后,第一件事就是到阎王殿报道接受审判。

十八层地狱:在设备管理一栏,可以看到各层地狱设备的运行状态、损耗情况等等。

六道轮回:轮回投胎是大事,不能儿戏,本管理系统设计的轮回盘既简单又科学。

冥币管理:无论身在何方,没钱都是走不通的。那个啥有钱能使鬼推磨!

日志管理:记录所有管理员的操作日志,对地府的工作人员进行绩效考核,合理评估是否进行编制扩充以及奖金方案。

你如何看地府后台管理系统这个项目?欢迎底部留言讨论。

正文结束

推荐阅读 ↓↓↓

1.不认命,从10年流水线工人,到谷歌上班的程序媛,一位湖南妹子的励志故事

2.如何才能成为优秀的架构师?

3.从零开始搭建创业公司后台技术栈

4.“37岁,985毕业,年薪50万,被裁掉只用了10分钟”

5.37岁程序员被裁,120天没找到工作,无奈去小公司,结果懵了...

6.副业&接私活必备的 10 个开源项目!

7.你知道哪10大算法统治着全球吗?

8.15张图看懂瞎忙和高效的区别!

一个人学习、工作很迷茫?

点击「阅读原文」加入我们的小圈子!

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
好的,我可以为你提供一个简单的Java.Swing万年历示例代码,如下所示: ```java import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; public class CalendarApp extends JFrame implements ActionListener { private static final long serialVersionUID = 1L; JLabel lblMonth, lblYear; JButton btnPrev, btnNext, btnExit; JTable tblCalendar; JComboBox<String> cmbYear; DefaultTableModel modelCalendar; public CalendarApp() { setTitle("Java Calendar"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(420, 320); lblMonth = new JLabel(); lblYear = new JLabel(); cmbYear = new JComboBox<String>(); btnPrev = new JButton("<<"); btnNext = new JButton(">>"); btnExit = new JButton("Exit"); modelCalendar = new DefaultTableModel() { private static final long serialVersionUID = 1L; public boolean isCellEditable(int rowIndex, int mColIndex) { return false; } }; tblCalendar = new JTable(modelCalendar); tblCalendar.addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent evt) { int row = tblCalendar.getSelectedRow(); int col = tblCalendar.getSelectedColumn(); int year = Integer.parseInt(cmbYear.getSelectedItem().toString()); int month = lblMonth.getText().equalsIgnoreCase("January") ? 0 : lblMonth.getText().equalsIgnoreCase("February") ? 1 : lblMonth.getText().equalsIgnoreCase("March") ? 2 : lblMonth.getText().equalsIgnoreCase("April") ? 3 : lblMonth.getText().equalsIgnoreCase("May") ? 4 : lblMonth.getText().equalsIgnoreCase("June") ? 5 : lblMonth.getText().equalsIgnoreCase("July") ? 6 : lblMonth.getText() .equalsIgnoreCase("August") ? 7 : lblMonth.getText() .equalsIgnoreCase("September") ? 8 : lblMonth.getText() .equalsIgnoreCase("October") ? 9 : lblMonth .getText() .equalsIgnoreCase( "November") ? 10 : 11; if (evt.getClickCount() == 2) { int day = (int) modelCalendar.getValueAt(row, col); new DayApp(day, month, year); } } }); JScrollPane paneCalendar = new JScrollPane(tblCalendar); paneCalendar.setPreferredSize(new Dimension(300, 200)); JPanel pnlCalendar = new JPanel(null); pnlCalendar.setBorder(BorderFactory.createTitledBorder("Calendar")); btnPrev.addActionListener(this); btnNext.addActionListener(this); cmbYear.addActionListener(this); btnExit.addActionListener(this); pnlCalendar.add(lblMonth); pnlCalendar.add(lblYear); pnlCalendar.add(cmbYear); pnlCalendar.add(btnPrev); pnlCalendar.add(btnNext); pnlCalendar.add(paneCalendar); pnlCalendar.add(btnExit); lblMonth.setBounds(160 - lblMonth.getPreferredSize().width / 2, 25, 100, 25); lblYear.setBounds(10, 225, 80, 20); cmbYear.setBounds(230, 225, 80, 20); btnPrev.setBounds(10, 25, 50, 25); btnNext.setBounds(300, 25, 50, 25); paneCalendar.setBounds(10, 50, 380, 160); btnExit.setBounds(160 - btnExit.getPreferredSize().width / 2, 250, 100, 25); GregorianCalendar cal = new GregorianCalendar(); int year = cal.get(GregorianCalendar.YEAR); int month = cal.get(GregorianCalendar.MONTH); int day = cal.get(GregorianCalendar.DAY_OF_MONTH); lblMonth.setText(String.format("%tB", cal)); lblYear.setText(String.format("%s", year)); for (int i = year - 100; i <= year + 100; i++) { cmbYear.addItem(String.format("%s", i)); } cmbYear.setSelectedItem(String.format("%s", year)); tblCalendar.getParent().setBackground(tblCalendar.getBackground()); tblCalendar.getTableHeader().setResizingAllowed(false); tblCalendar.getTableHeader().setReorderingAllowed(false); tblCalendar.setColumnSelectionAllowed(true); tblCalendar.setRowSelectionAllowed(true); tblCalendar.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); for (int i = 0; i < 7; i++) { modelCalendar.addColumn(new String[] { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }[i]); } tblCalendar.setRowHeight(38); modelCalendar.setColumnCount(7); modelCalendar.setRowCount(6); for (int i = year - 100; i <= year + 100; i++) { cmbYear.addItem(String.format("%s", i)); } refreshCalendar(month, year); add(pnlCalendar, BorderLayout.CENTER); setVisible(true); } public void refreshCalendar(int month, int year) { String[] months = new String[] { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; int nod, som; btnPrev.setEnabled(true); btnNext.setEnabled(true); if (month == 0 && year <= Calendar.getInstance().get(Calendar.YEAR) - 100) { btnPrev.setEnabled(false); } if (month == 11 && year >= Calendar.getInstance().get(Calendar.YEAR) + 100) { btnNext.setEnabled(false); } lblMonth.setText(months[month]); lblMonth.setBounds(160 - lblMonth.getPreferredSize().width / 2, 25, 100, 25); cmbYear.setSelectedItem(String.format("%s", year)); GregorianCalendar cal = new GregorianCalendar(year, month, 1); nod = cal.getActualMaximum(GregorianCalendar.DAY_OF_MONTH); som = cal.get(GregorianCalendar.DAY_OF_WEEK); for (int i = 0; i < 6; i++) { for (int j = 0; j < 7; j++) { modelCalendar.setValueAt(null, i, j); } } for (int i = 1; i <= nod; i++) { int row = new Integer((i + som - 2) / 7); int column = (i + som - 2) % 7; modelCalendar.setValueAt(i, row, column); } } public static void main(String[] args) { new CalendarApp(); } @Override public void actionPerformed(ActionEvent e) { if (e.getSource().equals(btnPrev)) { int year = Integer.parseInt(cmbYear.getSelectedItem().toString()); int month = lblMonth.getText().equalsIgnoreCase("January") ? 11 : lblMonth.getText().equalsIgnoreCase("February") ? 0 : lblMonth.getText().equalsIgnoreCase("March") ? 1 : lblMonth.getText().equalsIgnoreCase("April") ? 2 : lblMonth.getText().equalsIgnoreCase("May") ? 3 : lblMonth.getText().equalsIgnoreCase("June") ? 4 : lblMonth.getText().equalsIgnoreCase("July") ? 5 : lblMonth.getText() .equalsIgnoreCase("August") ? 6 : lblMonth.getText() .equalsIgnoreCase("September") ? 7 : lblMonth.getText() .equalsIgnoreCase("October") ? 8 : lblMonth.getText() .equalsIgnoreCase( "November") ? 9 : 10; if (month == 0) { month = 11; year--; } else { month--; } refreshCalendar(month, year); } else if (e.getSource().equals(btnNext)) { int year = Integer.parseInt(cmbYear.getSelectedItem().toString()); int month = lblMonth.getText().equalsIgnoreCase("January") ? 11 : lblMonth.getText().equalsIgnoreCase("February") ? 0 : lblMonth.getText().equalsIgnoreCase("March") ? 1 : lblMonth.getText().equalsIgnoreCase("April") ? 2 : lblMonth.getText().equalsIgnoreCase("May") ? 3 : lblMonth.getText().equalsIgnoreCase("June") ? 4 : lblMonth.getText().equalsIgnoreCase("July") ? 5 : lblMonth.getText() .equalsIgnoreCase("August") ? 6 : lblMonth.getText() .equalsIgnoreCase("September") ? 7 : lblMonth.getText() .equalsIgnoreCase("October") ? 8 : lblMonth.getText() .equalsIgnoreCase( "November") ? 9 : 10; if (month == 11) { month = 0; year++; } else { month++; } refreshCalendar(month, year); } else if (e.getSource().equals(cmbYear)) { int year = Integer.parseInt(cmbYear.getSelectedItem().toString()); int month = lblMonth.getText().equalsIgnoreCase("January") ? 11 : lblMonth.getText().equalsIgnoreCase("February") ? 0 : lblMonth.getText().equalsIgnoreCase("March") ? 1 : lblMonth.getText().equalsIgnoreCase("April") ? 2 : lblMonth.getText().equalsIgnoreCase("May") ? 3 : lblMonth.getText().equalsIgnoreCase("June") ? 4 : lblMonth.getText().equalsIgnoreCase("July") ? 5 : lblMonth.getText() .equalsIgnoreCase("August") ? 6 : lblMonth.getText() .equalsIgnoreCase("September") ? 7 : lblMonth.getText() .equalsIgnoreCase("October") ? 8 : lblMonth.getText() .equalsIgnoreCase( "November") ? 9 : 10; refreshCalendar(month, year); } else if (e.getSource().equals(btnExit)) { System.exit(0); } } } class DayApp extends JFrame implements ActionListener { private static final long serialVersionUID = 1L; JLabel lblDay, lblMonth, lblYear; JButton btnSave, btnCancel; JTextArea txtEvent; JPanel pnlDay; public DayApp(int day, int month, int year) { setTitle("Day Event"); setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); setSize(300, 200); setLocationRelativeTo(null); lblDay = new JLabel(String.format("%s", day)); lblMonth = new JLabel(String.format("%tB", new GregorianCalendar(year, month, 1))); lblYear = new JLabel(String.format("%s", year)); txtEvent = new JTextArea(); pnlDay = new JPanel(null); pnlDay.setBorder(BorderFactory.createTitledBorder("Event")); btnSave = new JButton("Save"); btnCancel = new JButton("Cancel"); btnSave.addActionListener(this); btnCancel.addActionListener(this); lblDay.setBounds(140 - lblDay.getPreferredSize().width / 2, 25, 100, 25); lblMonth.setBounds(10, 60, 100, 25); lblYear.setBounds(230, 60, 50, 25); txtEvent.setBounds(20, 90, 250, 50); btnSave.setBounds(50, 150, 80, 25); btnCancel.setBounds(160, 150, 80, 25); pnlDay.add(lblDay); pnlDay.add(lblMonth); pnlDay.add(lblYear); pnlDay.add(txtEvent); pnlDay.add(btnSave); pnlDay.add(btnCancel); add(pnlDay); setVisible(true); } @Override public void actionPerformed(ActionEvent e) { if (e.getSource().equals(btnSave)) { JOptionPane.showMessageDialog(this, "Event saved successfully!"); dispose(); } else if (e.getSource().equals(btnCancel)) { dispose(); } } } ``` 运行这个代码,你会看到一个简单的Java.Swing万年历。你可以选择年份和月份,然后浏览日历中的日期。你还可以双击一个日期来创建一个事件。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值