c# 如何实现自动更新程序

作者:keguoquan 时间:2021-11-20 21:02:41 

目录
  • 主要功能介绍

  • 客户端

    • main方法入口

    • 主窗体代码

    • 更新帮助类

    • 版本xml文件解析

  • 服务端

    • 版本xml文件

    • 自动升级服务Controller

    • 版本文件自动生成帮助类

  • 结语

    主要功能介绍

    实现文件的自动更新。主要功能:

    1. 支持整包完全更新,即客户端只需输入一个服务器地址,即可下载所有文件。

    2. 支持增量更新,即只更新指定的某几个文件。

    3. 支持自动更新程序的更新

    更新界面如图:

    c# 如何实现自动更新程序

    客户端

    main方法入口


    /// <summary>
    /// 应用程序的主入口点。
    /// </summary>
    [STAThread]
    static void Main()
    {
     //在主程序中 更新替换自动升级程序
     //ReplaceAutoUpgrade();

    bool isEnterMain = false;
     try
     {
     //设置默认更新地址,如果不设置,后面会从配置文件,或界面上进行设置
     UpgradeHelper.Instance.DefaultUrl = "http://localhost:17580";
     if (UpgradeHelper.Instance.Local_UpgradeModel != null)
     {
      UpgradeHelper.Instance.UpgradeUrl = UpgradeHelper.Instance.Local_UpgradeModel.UpgradeUrl;
     }

    if (UpgradeHelper.Instance.WillUpgrades.Count == 0 && UpgradeHelper.Instance.Local_UpgradeModel != null)
     {
      //没有待更新,并且本地版本信息文件不为空,则直接启动主程序
      bool isSucced = UpgradeHelper.StartRunMain(UpgradeHelper.Instance.Local_UpgradeModel.RunMain);
      if (isSucced)
      {
      Application.Exit();
      }
      else
      {
      //清理版本信息 以便重新检测版本
      UpgradeHelper.Instance.ClearUpgradeModel();
      isEnterMain = true;
      }
     }
     else
     {
      isEnterMain = true;
     }
     }
     catch (Exception ex)
     {
     isEnterMain = true;
     MessageBox.Show("运行更新程序异常:\n" + ex.Message, "错误提示", MessageBoxButtons.OK, MessageBoxIcon.Error);
     }

    if (isEnterMain)
     {
     //进入更新主界面
     Application.EnableVisualStyles();
     Application.SetCompatibleTextRenderingDefault(false);
     Application.Run(new FrmUpdate());
     }
    }

    主窗体代码


    public partial class FrmUpdate: Form
    {
    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="tempPath"></param>
    /// <param name="updateFiles"></param>
    public FrmUpdate()
    {
     InitializeComponent();
    }
    /// <summary>
    /// 窗体加载事件
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void FrmUpdate_Load(object sender, EventArgs e)
    {
     try
     {
     //加载服务器地址
     txtHostUrl.Text = UpgradeHelper.Instance.UpgradeUrl;
     BeginUpgrade();
     }
     catch(Exception ex)
     {
     Output("初始化异常:" + ex.Message);
     }
    }
    /// <summary>
    /// 手动更新
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void butBegin_Click(object sender, EventArgs e)
    {
    try
    {
     if(string.IsNullOrWhiteSpace(txtHostUrl.Text))
     {
     Output("请先输入服务器地址!");
     return;
     }
     UpgradeHelper.Instance.UpgradeUrl = txtHostUrl.Text.Trim();
     //清理版本信息 以便重新检测版本
     UpgradeHelper.Instance.ClearUpgradeModel();
     BeginUpgrade();
    }
    catch(Exception ex)
    {
     Output("更新异常:" + ex.Message);
    }
    }
    private void BeginUpgrade()
    {
     try
     {
     if(string.IsNullOrWhiteSpace(UpgradeHelper.Instance.UpgradeUrl))
     {
      return;
     }
     if(!(UpgradeHelper.Instance.UpgradeUrl.StartsWith("http://") || UpgradeHelper.Instance.UpgradeUrl.StartsWith("https://")))
     {
      Output("错误的服务器地址,地址必须以http://或者https://开头");
      return;
     }
     //判断是否有更新
     if(UpgradeHelper.Instance.WillUpgrades.Count > 0 && UpgradeHelper.Instance.Server_UpgradeModel != null)
     {
      SetWinControl(false);
      //杀死主进程
      UpgradeHelper.KillProcess(UpgradeHelper.Instance.Server_UpgradeModel.RunMain);
      RunUpgrade(); //启动更新
     }
     }
     catch(Exception ex)
     {
     Output("更新异常:" + ex.Message);
     }
    }
    /// <summary>
    /// 启动更新
    /// </summary>
    private void RunUpgrade()
    {
     //启动更新
     SetCaption(string.Format("共需更新文件{0}个,已更新0个。正在更新下列文件:", UpgradeHelper.Instance.WillUpgrades.Count));
     Task.Factory.StartNew(() =>
     {
     string curFile = "";
     try
     {
      int idx = 0;
      foreach(KeyValuePair < string, string > item in UpgradeHelper.Instance.WillUpgrades)
      {
       curFile = item.Key;
       string filePath = string.Format("{0}\\{1}", Application.StartupPath, item.Key);
       if(item.Key.IndexOf(UpgradeHelper.Instance.Server_UpgradeModel.AutoUpgrade) >= 0)
       {
       //如果当前文件为更新主程序
       filePath = string.Format("{0}\\AutoUpgradeTemp\\{1}", Application.StartupPath, item.Key);
       }
       string directory = Path.GetDirectoryName(filePath);
       if(!Directory.Exists(directory))
       {
       Directory.CreateDirectory(directory);
       }
       MyWebResquest.DownloadFile(UpgradeHelper.Instance.UpgradeUrl, item.Key, filePath);
       idx++;
       SetCaption(string.Format("共需更新文件{0}个,已更新{1}个。更新文件列表:", UpgradeHelper.Instance.WillUpgrades.Count, idx));
       Output(string.Format("更新文件{0}完成", curFile));
      }
      //保存版本文件
      File.WriteAllText(UpgradeHelper.Instance.Local_UpgradeXmlPath, UpgradeHelper.Instance.Server_UpgradeXml);
      SetCaption(string.Format("更新完成,共更新文件{0}个", UpgradeHelper.Instance.WillUpgrades.Count));
      Output(string.Format("更新完成,共更新文件{0}个", UpgradeHelper.Instance.WillUpgrades.Count));
      //下载完成后处理
      UpgradeHelper.StartRunMain(UpgradeHelper.Instance.Server_UpgradeModel.RunMain);
      //退出当前程序
      ExitCurrent();
     }
     catch(Exception ex)
     {
      Output(string.Format("更新文件{0}异常:{1}", curFile, ex.Message));
      SetWinControl(true);
     }
     });
    }
    /// <summary>
    /// 设置界面控件是否可用
    /// </summary>
    /// <param name="enabled"></param>
    private void SetWinControl(bool enabled)
    {
     if(this.InvokeRequired)
     {
     Action < bool > d = new Action < bool > (SetWinControl);
     this.Invoke(d, enabled);
     }
     else
     {
     txtHostUrl.Enabled = enabled;
     butBegin.Enabled = enabled;
     }
    }
    /// <summary>
    /// 退出当前程序
    /// </summary>
    private void ExitCurrent()
    {
    if(this.InvokeRequired)
    {
     Action d = new Action(ExitCurrent);
     this.Invoke(d);
    }
    else
    {
     Application.Exit();
    }
    }#
    region 日志输出
    /// <summary>
    /// 设置跟踪状态
    /// </summary>
    /// <param name="caption"></param>
    private void SetCaption(string caption)
    {
     if(this.lblCaption.InvokeRequired)
     {
     Action < string > d = new Action < string > (SetCaption);
     this.Invoke(d, caption);
     }
     else
     {
     this.lblCaption.Text = caption;
     }
    }
    /// <summary>
    /// 设置跟踪状态
    /// </summary>
    /// <param name="caption"></param>
    private void Output(string log)
    {
    if(this.txtLog.InvokeRequired)
    {
     Action < string > d = new Action < string > (Output);
     this.Invoke(d, log);
    }
    else
    {
     txtLog.AppendText(string.Format("{0}:{1}\r\n", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), log));
     txtLog.ScrollToCaret();
    }
    }
    private void ClearOutput()
    {
    if(this.txtLog.InvokeRequired)
    {
     Action d = new Action(ClearOutput);
     this.Invoke(d);
    }
    else
    {
     txtLog.Text = "";
    }
    }#
    endregion
    private void FrmUpdate_FormClosing(object sender, FormClosingEventArgs e)
    {
    if(e.CloseReason == CloseReason.UserClosing)
    {
     if(MessageBox.Show("升级未完成,退出后将导致软件无法正常使用,你确定要退出吗?", "退出提示", MessageBoxButtons.YesNo) != System.Windows.Forms.DialogResult.Yes)
     {
     //取消"关闭窗口"事件
     e.Cancel = true;
     }
    }
    }
    }

    更新帮助类


    /// <summary>
    /// 更新帮助类
    /// </summary>
    public class UpgradeHelper
    {
    /// <summary>
    /// 默认服务器地址
    /// 在配置文件中未找到地址时,使用此地址进行更新
    /// </summary>
    public string DefaultUrl
    {
    get;
    set;
    }
    public string _upgradeUrl;
    /// <summary>
    /// 获取或设置服务器地址
    /// </summary>
    public string UpgradeUrl
    {
    get
    {
     if(string.IsNullOrWhiteSpace(_upgradeUrl))
     {
     return DefaultUrl;
     }
     return _upgradeUrl;
    }
    set
    {
     _upgradeUrl = value;
    }
    }
    /// <summary>
    /// 本地配置文件路径
    /// </summary>
    public string Local_UpgradeXmlPath = Path.Combine(Application.StartupPath, "UpgradeList.xml");
    private UpgradeModel _local_UpgradeModel;
    /// <summary>
    /// 本地版本信息
    /// </summary>
    public UpgradeModel Local_UpgradeModel
    {
    get
    {
     try
     {
     if(_local_UpgradeModel == null)
     {
      if(File.Exists(Local_UpgradeXmlPath))
      {
      _local_UpgradeModel = new UpgradeModel();
      _local_UpgradeModel.LoadUpgrade(File.ReadAllText(Local_UpgradeXmlPath));
      }
     }
     return _local_UpgradeModel;
     }
     catch(Exception ex)
     {
     throw new Exception(string.Format("获取本地版本文件UpgradeList.xml异常:{0}", ex.Message));
     }
    }
    }
    private UpgradeModel _server_UpgradeModel;
    /// <summary>
    /// 服务器版本信息
    /// </summary>
    public UpgradeModel Server_UpgradeModel
    {
    get
    {
     try
     {
     if(_server_UpgradeModel == null && !string.IsNullOrWhiteSpace(UpgradeUrl))
     {
      string resXml = MyWebResquest.GetUpgradeList(UpgradeUrl);
      if(!string.IsNullOrWhiteSpace(resXml))
      {
      _server_UpgradeModel = new UpgradeModel();
      _server_UpgradeModel.LoadUpgrade(resXml);
      _server_UpgradeXml = resXml;
      }
     }
     return _server_UpgradeModel;
     }
     catch(Exception ex)
     {
     throw new Exception(string.Format("获取服务端版本文件UpgradeList.xml异常:{0}", ex.Message));
     }
    }
    }
    private string _server_UpgradeXml;
    /// <summary>
    /// 服务端版本配置xml
    /// </summary>
    public string Server_UpgradeXml
    {
    get
    {
     return _server_UpgradeXml;
    }
    }
    private Dictionary < string, string > _willUpgrades;
    /// <summary>
    /// 待更新文件列表,如果为0,则表示不需要更新
    /// </summary>
    public Dictionary < string, string > WillUpgrades
    {
     get
     {
     if(_willUpgrades == null)
     {
      _willUpgrades = new Dictionary < string, string > ();
      //如果服务器端未获取到版本信息 则不更新
      if(Server_UpgradeModel != null)
      {
      if(Local_UpgradeModel == null) //本地版本信息为空 全部更新
      {
       _willUpgrades = Server_UpgradeModel.DictFiles;
      }
      else
      {
       //对比需要更新的文件
       foreach(var item in Server_UpgradeModel.DictFiles)
       {
       //如果找到
       if(Local_UpgradeModel.DictFiles.ContainsKey(item.Key))
       {
        //如果版本不匹配
        if(Local_UpgradeModel.DictFiles[item.Key] != item.Value)
        {
        _willUpgrades.Add(item.Key, item.Value);
        }
       }
       else
       {
        //没有找到
        _willUpgrades.Add(item.Key, item.Value);
       }
       }
      }
      }
     }
     return _willUpgrades;
     }
    }
    /// <summary>
    /// 清空版本信息
    /// </summary>
    public void ClearUpgradeModel()
    {
    if(File.Exists(Local_UpgradeXmlPath))
    {
     try
     {
     string xmlStr = File.ReadAllText(Local_UpgradeXmlPath);
     XmlDocument xmlDoc = new XmlDocument();
     xmlDoc.LoadXml(xmlStr);
     XmlNode node = xmlDoc.SelectSingleNode("Upgrade/Files");
     if(node != null && node.ChildNodes.Count > 0)
     {
      node.RemoveAll();
     }
     File.WriteAllText(UpgradeHelper.Instance.Local_UpgradeXmlPath, xmlDoc.InnerXml);
     }
     catch(Exception)
     {}
    }
    _local_UpgradeModel = null;
    _server_UpgradeModel = null;
    _willUpgrades = null;
    }#
    region 单例对象
    private static UpgradeHelper _instance;
    /// <summary>
    /// 单例对象
    /// </summary>
    public static UpgradeHelper Instance
    {
    get
    {
     if(_instance == null)
     {
     _instance = new UpgradeHelper();
     //初始化本地配置文件,以及服务器地址
     if(_instance.Local_UpgradeModel != null)
     {
      _instance.UpgradeUrl = _instance.Local_UpgradeModel.UpgradeUrl;
     }
     }
     return _instance;
    }
    }#
    endregion# region 静态方法
    /// <summary>
    /// 启动主程序
    /// </summary>
    /// <param name="fileName"></param>
    public static bool StartRunMain(string fileName)
    {
     string fullPath = fileName;
     try
     {
     Process process = GetProcess(fileName);
     if(process != null) //以及存在运行中的主进程
     {
      return true;
     }
     fullPath = string.Format("{0}\\{1}", Application.StartupPath, fileName);
     ProcessStartInfo main = new ProcessStartInfo(fullPath);
     Process.Start(fullPath);
     return true;
     }
     catch(Exception ex)
     {
     MessageBox.Show(string.Format("主程序{0}调用失败:\n{1}", fullPath, ex.Message), "错误提示", MessageBoxButtons.OK, MessageBoxIcon.Error);
     }
     return false;
    }
    /// <summary>
    /// 杀死进程
    /// </summary>
    /// <param name="process"></param>
    public static void KillProcess(string processName)
    {
     if(string.IsNullOrWhiteSpace(processName)) return;
     processName = processName.ToLower();
     processName = processName.Replace(".exe", "");
     //杀死主进程
     Process[] processes = Process.GetProcesses();
     foreach(Process process in processes)
     {
     if(!string.IsNullOrWhiteSpace(process.ProcessName))
     {
      if(process.ProcessName.ToLower() == processName)
      {
      process.Kill();
      }
     }
     }
    }
    /// <summary>
    /// 获取进程
    /// </summary>
    /// <param name="pName"></param>
    /// <returns></returns>
    public static Process GetProcess(string pName)
    {
    if(string.IsNullOrWhiteSpace(pName)) return null;
    pName = pName.ToLower();
    pName = pName.Replace(".exe", "");
    //杀死主进程
    Process[] processes = Process.GetProcesses();
    foreach(Process process in processes)
    {
     if(!string.IsNullOrWhiteSpace(process.ProcessName))
     {
     if(process.ProcessName.ToLower() == pName)
     {
      return process;
     }
     }
    }
    return null;
    }#
    endregion
    }

    版本xml文件解析


    public class UpgradeModel
    {
    /// <summary>
    /// 初始化对象
    /// </summary>
    /// <param name="xml"></param>
    public void LoadUpgrade(string xml)
    {
    XmlDocument xmlDoc = new XmlDocument();
    xmlDoc.LoadXml(xml);
    //读取UpgradeUrl
    XmlNode node = xmlDoc.SelectSingleNode("//UpgradeUrl");
    if(node != null)
    {
     this.UpgradeUrl = node.InnerText;
    }
    //读取RunMain
    node = xmlDoc.SelectSingleNode("//RunMain");
    if(node != null)
    {
     this.RunMain = node.InnerText;
    }
    //读取RunMain
    node = xmlDoc.SelectSingleNode("//AutoUpgrade");
    if(node != null)
    {
     this.AutoUpgrade = node.InnerText;
    }
    //读取Files
    node = xmlDoc.SelectSingleNode("Upgrade/Files");
    this.DictFiles = new Dictionary < string, string > ();
    if(node != null && node.ChildNodes.Count > 0)
    {
     foreach(XmlNode item in node.ChildNodes)
     {
     if(item.Name != "#comment")
     {
      string name = GetNodeAttrVal(item, "Name");
      string version = GetNodeAttrVal(item, "Version");
      if(!this.DictFiles.ContainsKey(name))
      {
      this.DictFiles.Add(name, version);
      }
     }
     }
    }
    }
    private string GetNodeAttrVal(XmlNode node, string attr)
    {
     if(node != null && node.Attributes != null && node.Attributes[attr] != null)
     {
     string val = node.Attributes[attr].Value;
     if(!string.IsNullOrWhiteSpace(val))
     {
      return val.Trim();
     }
     return val;
     }
     return string.Empty;
    }
    /// <summary>
    /// 服务器地址
    /// </summary>
    public string UpgradeUrl
    {
    get;
    set;
    }
    /// <summary>
    /// 更新完成后运行的主程序名称
    /// </summary>
    public string RunMain
    {
    get;
    set;
    }
    /// <summary>
    /// 更新程序名称
    /// </summary>
    public string AutoUpgrade
    {
    get;
    set;
    }
    /// <summary>
    /// 文件列表
    /// string 文件名
    /// string 版本号
    /// </summary>
    public Dictionary < string, string > DictFiles
    {
    get;
    set;
    }
    }

    服务端

    服务端主Xml版本文件,包含所有的项目文件,客户端根据每个文件的版本号进行判断是否需要更新。如果需只更新某几个文件,则将对应文件的版本号更改只更高的版本号即可

    版本xml文件


    <?xml version="1.0" encoding="utf-8" ?>
    <Upgrade>
    <!--服务器地址-->
    <UpgradeUrl>http://localhost:17580</UpgradeUrl>
    <!--更新完成后运行的主程序名称-->
    <RunMain>ClientMain.exe</RunMain>
    <!--更新程序名称-->
    <AutoUpgrade>AutoUpgrade.exe</AutoUpgrade>
    <Files>
    <!--更新文件列表,以Version为标志,当Version改变时,客户端启动会自动更新。子路径格式:\image\index.jpg-->
    <File Version="01" Name="\image\index.jpg" />
    <File Version="01" Name="ClientMain.exe" />
    <File Version="01" Name="AutoUpgrade.exe" />
    </Files>
    </Upgrade>

    服务端主要提供连个可以通过Http的get或post访问的路径。一个用于获取版本Xml文件内容,一个用于下载指定文件的路径。以下代码示例通过asp.net mvc进行实现。大家可以根据自己技术方式参照实现。

    自动升级服务Controller


    /// <summary>
    /// 自动升级服务
    /// </summary>
    public class UpgradeController: Controller
    {
    //
    // GET: /Upgrade/
    /// <summary>
    /// 获取更新文件列表
    /// </summary>
    /// <returns></returns>
    public object UpgradeList()
    {
     string cacheKey = "Upgrade_UpgradeList.xml";
     string resStr = CommonLibrary.CacheClass.GetCache < string > (cacheKey);
     if(string.IsNullOrWhiteSpace(resStr))
     {
     string fileName = Server.MapPath(@"~\App_Data\UpgradeList.xml");
     if(System.IO.File.Exists(fileName))
     {
      resStr = System.IO.File.ReadAllText(fileName);
      CommonLibrary.CacheClass.SetCacheMins(cacheKey, resStr, 1);
     }
     }
     return resStr;
    }
    /// <summary>
    /// 生成更新文件
    /// </summary>
    /// <returns></returns>
    public object Create()
    {
     UpgradeFileManager.CreateFiles(Server.MapPath("/App_Data"));
     return "ok";
    }
    /// <summary>
    /// 下载文件
    /// </summary>
    /// <param name="fileName"></param>
    /// <returns></returns>
    public object DownloadFile()
    {
     string fileName = PageRequest.GetString("fileName");
     fileName = Server.MapPath(string.Format(@"~\App_Data\{0}", fileName));
     return File(fileName, "application/octet-stream");
    }
    /// <summary>
    /// 异常处理
    /// </summary>
    /// <param name="filterContext"></param>
    protected override void OnException(ExceptionContext filterContext)
    {
    filterContext.HttpContext.Response.StatusCode = 400;
    filterContext.Result = Content(filterContext.Exception.GetBaseException().Message);
    filterContext.ExceptionHandled = true;
    }
    }

    版本文件自动生成帮助类


    /// <summary>
    /// 此类主要作用,对于项目文件非常多,自己手动编辑很麻烦,可以采用此方法,指定目录自动生成初始化的版本文件
    /// </summary>
    public class UpgradeFileManager
    {
    /// <summary>
    /// 创建版本文件
    /// </summary>
    /// <param name="path"></param>
    public static void CreateFiles(string path)
    {
     List < string > dirList = new List < string > ();
     GetAllDirt(path, dirList); //获取所有目录
     dirList.Add(path);
     System.Text.StringBuilder xml = new System.Text.StringBuilder();
     xml.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\" ?>");
     xml.AppendLine(" <Files>");
     foreach(var diry in dirList)
     {
     string[] files = Directory.GetFiles(diry);
     foreach(string filePath in files)
     {
      FileInfo info = new FileInfo(filePath);
      string name = filePath.Replace(path, "");
      if(info.Directory.FullName == path)
      {
      name = name.Remove(0, 1);
      }
      xml.AppendLine(string.Format(" <File Version=\"1\" Name=\"{0}\" />", name));
     }
     }
     xml.AppendLine("</Files>");
     using(StreamWriter sw = new StreamWriter(Path.Combine(path, "UpgradeList_Temp.xml")))
     {
     sw.Write(xml);
     sw.Close();
     }
    }
    /// <summary>
    /// 获取所有子目录
    /// </summary>
    /// <param name="curDir"></param>
    /// <param name="list"></param>
    private static void GetAllDirt(string curDir, List < string > list)
    {
    string[] dirs = Directory.GetDirectories(curDir);
    if(dirs.Length > 0)
    {
     foreach(string item in dirs)
     {
     list.Add(item);
     GetAllDirt(item, list);
     }
    }
    }
    }

    结语

    源代码托管于GitHub,供大伙学习参考,项目地址:https://github.com/keguoquan/AutoUpgrade。感兴趣或觉得不错的望赏个star,不胜感激!

    若能顺手点个赞,更加感谢!

    来源:https://www.cnblogs.com/keguoquan/p/14542631.html

    标签:c#,自动更新,程序
    0
    投稿

    猜你喜欢

  • Java 对HashMap进行排序的三种常见方法

    2022-11-04 19:02:10
  • Eclipse的Debug调试技巧大全(总结)

    2023-11-25 06:14:06
  • 关于SpringMVC的异常处理机制详细解读

    2023-01-26 17:32:45
  • Java设计模式初识之备忘录模式详解

    2023-08-29 23:27:09
  • IDEA Ui设计器JFormDesigner 永久激活插件+注册机(亲测一直在用)

    2022-10-23 18:44:46
  • C#实现按照指定长度在数字前补0方法小结

    2023-02-23 09:42:32
  • SpringBoot参数校验Validator框架详解

    2023-09-22 07:08:40
  • spring-boot-autoconfigure模块用法详解

    2023-11-25 12:59:19
  • Java实现红黑树(平衡二叉树)的详细过程

    2021-08-08 15:03:12
  • Android入门简单实例

    2021-07-26 16:34:04
  • Android对话框AlertDialog详解

    2023-06-20 01:47:19
  • Java程序设计之12个经典样例

    2022-09-22 18:44:00
  • 分享java中设置代理的两种方式

    2023-10-28 10:48:52
  • Java实现中国象棋的示例代码

    2021-08-10 21:56:03
  • 详解Android的.aar文件生成方法以及使用技巧

    2023-08-06 06:43:20
  • Java数据结构之图的基础概念和数据模型详解

    2022-03-25 11:27:04
  • Java编程实现五子棋人人对战代码示例

    2021-12-27 12:58:51
  • 如何安装java的运行环境IDEA

    2022-09-20 10:42:38
  • 这一次搞懂Spring事务是如何传播的

    2022-12-27 07:06:53
  • @Configuration与@Component作为配置类的区别详解

    2023-03-09 19:50:15
  • asp之家 软件编程 m.aspxhome.com