2013年4月4日

ASP.NET 网站维护实例

我3月21日回到学校,就被告知,某个部门的网站坏了。最初是一个Excel导出功能无法正常工作,导出的文件并不是先前期望的那样。由于这并不是本文重点,我就一笔带过——问题在于Excel的DCOM配置,添加了NETSERVICE账户并赋予相应权限后,得以解决。随之而来的问题是:导出的文件没法从服务器上删除。我们又添加了log4net,输出了一些调试信息,发现是最后导出文件结束、关闭Excel进程时出的问题。网站以NETSERVICE这个账户运行,但Excel是以Administrator运行的,在执行Process.Kill()方法时会抛出异常。虽然作者在里面使用了try-catch块来处理异常,但似乎这个异常仍然没被catch到,抛上来,后面删除文件的代码也就没有执行。

#region 把数据导入到.xls文件2-通过下载附件
    public void ExportToExcel(DataSet MySet, string[] phaseName, string strNewfile,string headstr)
    {
        //使用自动化导出Excel表格数据文件
        Excel.Application MyExcel;
        Excel.Workbooks MyWorkBooks;
        Excel.Workbook MyWorkBook;
        Excel.Worksheet MyWorkSheet;
        int Count, m = 1;
        int totalrow = 0;
        try
        {
            Object MyObj = System.Reflection.Missing.Value;
            MyExcel = new Excel.Application();
            MyExcel.Visible = false;
            if (MyExcel == null)
            {
                System.Diagnostics.Debug.WriteLine("Excel程序无法启动!");
                return;
            }
            MyWorkBooks = MyExcel.Workbooks;
            MyWorkBook = MyWorkBooks.Add(MyObj);
            MyWorkSheet = (Excel.Worksheet)MyWorkBook.Worksheets[1];
            MyWorkSheet.Activate();
            Count = 0;
            try
            {
                for (int n = 0; n < phaseName.Length; n++)
                {
                    MyWorkSheet.Cells[3, m] = phaseName[n].ToString();
                    ++m;
                    Count = Count + 1;
                }

                int index = headstr.Trim().IndexOf("&");
                string head1 = headstr.Trim().Substring(0, index);
                string head2 = headstr.Trim().Substring(index + 1, headstr.Trim().Length - 1 - index);
                //大标题------------------------------------------------------------------------------  
                MyWorkSheet.get_Range(MyWorkSheet.Cells[1, 1], MyWorkSheet.Cells[1, Count]).Merge(true);
                MyWorkSheet.get_Range(MyWorkSheet.Cells[1, 1], MyWorkSheet.Cells[1, Count]).Value2 = head1;
                MyWorkSheet.get_Range(MyWorkSheet.Cells[1, 1], MyWorkSheet.Cells[1, Count]).Font.Bold = true;
                MyWorkSheet.get_Range(MyWorkSheet.Cells[1, 1], MyWorkSheet.Cells[1, Count]).Font.Size = 18;
                MyWorkSheet.get_Range(MyWorkSheet.Cells[1, 1], MyWorkSheet.Cells[1, Count]).HorizontalAlignment = Excel.XlHAlign.xlHAlignCenter;

                MyWorkSheet.get_Range(MyWorkSheet.Cells[2, 1], MyWorkSheet.Cells[2, Count]).Merge(true);
                MyWorkSheet.get_Range(MyWorkSheet.Cells[2, 1], MyWorkSheet.Cells[2, Count]).Value2 = head2;
                MyWorkSheet.get_Range(MyWorkSheet.Cells[2, 1], MyWorkSheet.Cells[2, Count]).Font.Bold = true;
                MyWorkSheet.get_Range(MyWorkSheet.Cells[2, 1], MyWorkSheet.Cells[2, Count]).Font.Size = 18;
                MyWorkSheet.get_Range(MyWorkSheet.Cells[2, 1], MyWorkSheet.Cells[2, Count]).HorizontalAlignment = Excel.XlHAlign.xlHAlignCenter;
                //------------------------------------------------------------------------------------
                //设标题为黑体字             
                MyWorkSheet.get_Range(MyWorkSheet.Cells[3, 1], MyWorkSheet.Cells[3, Count]).Font.Name = "宋体";
                //标题字体加粗          
                MyWorkSheet.get_Range(MyWorkSheet.Cells[3, 1], MyWorkSheet.Cells[3, Count]).Font.Bold = false;
                //标题文字居中
                MyWorkSheet.get_Range(MyWorkSheet.Cells[3, 1], MyWorkSheet.Cells[3, Count]).HorizontalAlignment = 3;
                //标题单元格宽度
                MyWorkSheet.get_Range(MyWorkSheet.Cells[3, 1], MyWorkSheet.Cells[3, Count]).EntireColumn.AutoFit();
                //数字显示格式
                totalrow = MySet.Tables[0].Rows.Count;
                //设表格边框样式          
                MyWorkSheet.get_Range(MyWorkSheet.Cells[3, 1], MyWorkSheet.Cells[totalrow + 3, Count]).Borders.LineStyle = 1;
                MyWorkSheet.get_Range(MyWorkSheet.Cells[4, 2], MyWorkSheet.Cells[totalrow + 3, Count]).NumberFormatLocal = "0.00_ ";
                //数字自动显示
                MyWorkSheet.get_Range(MyWorkSheet.Cells[1, 1], MyWorkSheet.Cells[totalrow + 3, Count]).EntireColumn.AutoFit();
                //输出数据库记录
                for (int i = 0; i < MySet.Tables[0].Rows.Count; i++)
                {
                    for (int j = 0; j < MySet.Tables[0].Columns.Count; j++)
                    {
                        MyWorkSheet.Cells[i + 4, j + 1] = MySet.Tables[0].Rows[i][j].ToString();
                    }
                }
            }
            catch (Exception e)
            {
                System.Diagnostics.Debug.WriteLine("向excel文件中写入数据出错: " + e.Message);
            }
            finally
            {
                MyExcel.ActiveWorkbook._SaveAs(strNewfile, MyObj, MyObj, MyObj, MyObj, MyObj, XlSaveAsAccessMode.xlNoChange, MyObj, MyObj, MyObj, MyObj);
                MyExcel.ActiveWorkbook.Close(null, null, null);
                MyExcel.Workbooks.Close();
                MyExcel.Application.Quit();
                MyExcel.Quit();
                System.Runtime.InteropServices.Marshal.ReleaseComObject(MyExcel);
                MyExcel = null;
                //取得Excel进程列表,并杀死
                System.Diagnostics.Process[] excel_processes = System.Diagnostics.Process.GetProcessesByName("EXCEL");
                foreach (System.Diagnostics.Process excel_process in excel_processes)
                    excel_process.Kill();  
                System.GC.Collect();
            }
        }

        catch (Exception MyEx)
        {
            System.Diagnostics.Debug.WriteLine(MyEx.Message);
        }
    }
    #endregion   

最后我们只好在调用此方法的方法中再用try-catch把它包起来,换用Process.Dispose()——勉强解决了这个问题——始终有2个EXCEL进程,但不会更多。

我们刚消停了几天,又有新问题来了:在一个页面中稍微执行几次更新数据库数据的操作就会出现连接池已满的问题。当时我的第一反应是DataReader使用后没有及时关闭,但查看代码后发现那个方法并没有使用SqlDataReader。再一看这个方法里大量使用了他自己写的一个访问数据库的方法。本来程序中已经使用Microsoft.ApplicationBlocks.Data(即SqlHelper)来访问数据库了,我实在不能理解为什么作者还要再自己写一个访问数据库的方法。即使是想再次封装SqlHelper中的方法,在最后执行数据库操作的时候也应该使用SqlHelper中的方法,而不是自己从头再来写一次。这种手边拿着个轮子还要现场重新发明轮子的做法我真是第一次见。

正是由于作者自己又去封装了一个访问数据库的类,这就埋下了祸根。请看这个类中的其中两个方法:

/// <summary>
/// 获取数据库连接
/// </summary>
/// <returns>一个开启的数据库连接</returns>
public SqlConnection ExceCon()
{
    SqlConnection Con = new SqlConnection(ConfigurationManager.AppSettings["connStr"]);
    Con.Open();
    return Con;
}

/// <summary>
/// 执行SQL语句
/// </summary>
/// <param name="cmdtxt">要执行的SQL语句</param>
/// <returns></returns>
public bool ExceSQL(string cmdtxt)
{
    SqlCommand Com = new SqlCommand(cmdtxt, ExceCon());
    try
    {
        Com.ExecuteNonQuery();
        return true;
    }
    catch
    {
        return false;
    }
    finally
    {
       ExceCon().Close();
    }
}

/// <summary>
/// 返回一个DataSet数据类型的数据
/// </summary>
/// <param name="cmdtxt">要执行的SQL语句</param>
/// <param name="tblName">要绑定的数据表</param>
/// <returns></returns>
public DataSet ExceDS(string cmdtxt, string tblName)
{
    SqlConnection Con = ExceCon();
    SqlCommand Com;
    DataSet ds = null;
    try
    {
        Com = new SqlCommand(cmdtxt, Con);
        SqlDataAdapter Da = new SqlDataAdapter();
        Da.SelectCommand = Com;
        ds = new DataSet(tblName);
        Da.Fill(ds);
    }
    catch (Exception ex)
    {
       Con.Close();     
    }
    return ds;
}

这个类中的所有方法几乎都是如此的套路。不难发现,ExceCon()方法每次执行均会打开并返回一个新的连接。我们来看ExceSQL()方法,比较醒目的是finally中的代码,它获得了一个打开的连接(ExceCon()方法)然后立即把它关闭了(Close()方法)。这令人哭笑不得,原作者似乎认为这样可以关闭掉最初执行ExceCon()方法时开启的连接。当然,每次调用这个方法时未关闭的连接只有1个而不是2个,这点也许还能令人略感欣慰。

而ExceDS()方法中的代码就更扯蛋了:这个打开的连接只有在出现异常的情况下才会被关闭,正确执行时该连接永远不会被关闭。于是我逐个修复这个类中的方法,好在数量不多,还算顺利。修改后的代码如下:

/// <summary>
/// 执行SQL语句
/// Path by Tsclab 2013.4.4
/// </summary>
/// <param name="cmdtxt">要执行的SQL语句</param>
/// <returns></returns>
public bool ExceSQL(string cmdtxt)
{
    //Path by Tsclab
    SqlConnection conn = ExceCon();
    //SqlCommand Com = new SqlCommand(cmdtxt, ExceCon());
    SqlCommand Com = new SqlCommand(cmdtxt, conn);
    try
    {
        Com.ExecuteNonQuery();
        return true;
    }
    catch(Exception ex)
    {
        log.Error(ex);
        return false;
    }
    finally
    {
        //Path by Tsclab
        //以下代码的功能为打开一个新的连接并关闭,不能关闭new SqlCommand()中的连接
        //ExceCon().Close();
        conn.Close();
    }
}

/// <summary>
/// 返回一个DataSet数据类型的数据
/// Path by Tsclab 2013.4.4
/// </summary>
/// <param name="cmdtxt">要执行的SQL语句</param>
/// <param name="tblName">要绑定的数据表</param>
/// <returns></returns>
public DataSet ExceDS(string cmdtxt, string tblName)
{
    SqlConnection Con = ExceCon();
    SqlCommand Com;
    DataSet ds = null;
    try
    {
        Com = new SqlCommand(cmdtxt, Con);
        SqlDataAdapter Da = new SqlDataAdapter();
        Da.SelectCommand = Com;
        ds = new DataSet(tblName);
        Da.Fill(ds);
    }
    catch (Exception ex)
    {
        //Con.Close();     /*原作者代码*/
        
        //Patch by Tsclab
        //Log error here.   
        log.Error(ex);
    }
    finally
    {
        Con.Close();
    }
    /*Patch finished*/
    return ds;
}   

最初我们曾怀疑是SqlHelper中的方法没有关闭连接,我们反编译了这个DLL看到了如下的代码:

我以为在using语句块中执行return语句程序会直接从这里跳出这个方法并返回,using的Connection无法关闭。但后来我做了测试证明,这样的写法也是可以正常关闭连接的。

PS 该网站使用了MagicAjax,开发这个组件的公司好像已经不存在了(其网站上不去了)。当使用.NET 4.0环境来运行网站时,不能正常工作,使用.NET 2.0时则可以正常工作。原因是.NET 4.0不再为ASP.NET的页面中<form>添加name属性,导致document.form1["name"]这样的JS代码无法正确执行。.NET 2.0则不存在这样的问题,而MagicAjax本身也是为.NET 2.0开发的。

P.S. 2我怀疑这个网站是由2个或2个以上的程序员完成的,不然实在无法解释为什么会出现“手里抱着现成的轮子还要去重新发明轮子”这种奇怪的现象。

没有评论:

发表评论