代码改变世界

(转载)关于C# 全局异常捕捉

2016-10-24 17:39  mi-战斧  阅读(14453)  评论(0编辑  收藏  举报

  

C# 全局异常捕获

开发界有那么一个笑话,说是“「我爱你」三个字,讲出来只要三秒钟,解释要三小时,证明却要一辈子。「Bug」三个字母,发现需要三秒,找到需要三小时,Debug却要一辈子。”。就算是资深的程序员也会写出Bug,但Bug并不可怕,重要的是在Bug发生的时候迅速定位Bug。

在Visual Studio中调试的时候,我们可以借助VS的调试工具进行调试,一旦出现未处理的异常时,VS也会在第一时间捕获并显示出来。随着开发的进行,终于程序要打包上线了。那么在上线之出了BUG我们该如何处理呢?

 

相信如果各位年龄够大,应该都见识过QQ出错崩溃吧?在零几年的时候QQ崩溃还不是一件稀罕事儿。每当QQ崩溃的时候都会弹出一个BUGReporter程序,会希望我们将出错的报告发送给腾讯

 

其实我们标题所说的全局异常捕获主要目标并不是为了将异常处理掉防止程序崩溃。因为当错误被你的全局异常捕获器抓到的时候,已经证实了你程序中存在BUG。一般而言,我们的全局异常捕获主要作用就是接收到异常之后进行异常的反馈。

 

一、简单粗暴:在Program.cs使用Try...Catch...

大家都知道,异常是通过Throw命令抛出,一路从抛出的模块里上抛,如果中途没有被try...catch...抓住的话就会一直抛到CLR(公共语言运行时)。如果用栈来描述这个过程的话,那就是异常会从栈的栈顶一路下沉,直到中途被try...catch...抓住或者直至沉到栈底,被CLR接住。CLR接收到异常之后的处理方式非常的简单粗暴——直接报错,然后关闭程序。大概表现就是这样:

 

这样:

 

还有这样:

 

不过根据刚刚我们所描述的异常上抛过程,我们不难发现:只要我们在程序把异常抛给CLR之前,抢先把异常捕获,那就可以做到全局异常处理了。不过这个try...catch...就必须放在栈的最下方。程序运行时栈的最下方函数其实就是程序运行时第一个调用的函数——main()函数。

比如说我有这么一个Windows窗体程序,一个Program.cs类,一个FrmMain.cs窗口。

Program.cs类内容如下:

static class Program
{
    /// <summary>
    /// 应用程序的主入口点。
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new FrmMain());
    }
}

 

FrmMain里有一个Button,其点击事件如下:

private void button1_Click(object sender, EventArgs e)
{
    throw new Exception();
}

 

如果就现在这样的代码而言,只要在运行时单击这个Button就能时整个程序报错崩溃。

 

现在我们改造Program.cs里的main()函数,改成以下内容:

/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main()
{
    try
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new FrmMain());
    }
    catch (Exception ex)
    {
        MessageBox.Show(string.Format("捕获到未处理异常:{0}\r\n异常信息:{1}\r\n异常堆栈:{2}", ex.GetType(), ex.Message, ex.StackTrace));
    }
}

 

现在再次执行程序,单击button1,我们可以看到现在这次的这个异常被main()函数中的try...catch...所抓获了。

 

我们在这里catch内只是放了一个MessageBox,后面可以根据自己的需要改成错误报告代码。

 

二、更优雅的事件监听:Application.ThreadException

刚刚在上面提到了一种简单粗暴的方法,就是用try...catch...把main()函数所有内容全部包住。不过这样的代码看起来就有点蠢了。有没有什么更加优雅的方法吗?

答案当然是有。还记得Application类吧,这个负责控制整个Windows 程序的运行。我们可能用到这个类的时候更多的时候用的是下面几个方法或属性:

 

今天在这里我们要用到一个Application类的一个事件:ThreadException。

我们可以按F12转到Application类的定义,在ThreadException事件上,微软对它的注释是:“在发生未捕获线程异常时发生。”,这正是我们的目标。

 

假设还是之前的那个程序,我们将程序的Program.cs内容填入以下代码:

static class Program
{
    /// <summary>
    /// 应用程序的主入口点。
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.ThreadException += Application_ThreadException;
         
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new FrmMain());
    }
 
    static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
    {
        Exception ex = e.Exception;
        MessageBox.Show(string.Format("捕获到未处理异常:{0}\r\n异常信息:{1}\r\n异常堆栈:{2}", ex.GetType(), ex.Message, ex.StackTrace));
    }
}

 

现在我们重新运行这个程序,单击button1,查看效果:

 

可以说效果出乎意料的完美,上一章里在main()函数里用try..catch...包住所有代码,当程序异常时将代码将会进入catch块里,处理完成后程序就退出了。然而在这边我们用Application.ThreadException事件监听并处理后,程序并不会因为异常而退出。可以说是可挽回的异常。

 

三、子线程异常捕获AppDomain.CurrentDomain.UnhandledException

在上面我们提到了可以通过监听Application.ThreadException事件来捕获程序异常,但这个Application.ThreadException事件只能捕获程序主线程上发生的异常。如果你用到了多线程,而且在子线程中发生了异常,不会触发Application.ThreadException事件的。

如果要监听子线程的异常,我们就需要再注册一个事件:AppDomain.CurrentDomain.UnhandledException

这个事件是在当前程序域内发生未处理异常时才会发生(如果没有监听Application.ThreadException事件的话,主线程异常最终也会触发这个事件)

我们在第一章的窗口里再加一个Button按钮,名为button2,其单击事件内容为:

private void button2_Click(object sender, EventArgs e)
{
    Thread thread = new Thread(() =>
    {
        throw new Exception();
    });
    thread.Start();
}

 

上面的代码会在运行时创建一个线程,线程代码只有一句“throw new Exception();”致使线程抛出异常。

我们再改一下Program.cs类的内容:

static class Program
{
    /// <summary>
    /// 应用程序的主入口点。
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.ThreadException += Application_ThreadException;
        AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
         
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new FrmMain());
    }
     
    static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
    {
        Exception ex = e.ExceptionObject as Exception;
        MessageBox.Show(string.Format("捕获到未处理异常:{0}\r\n异常信息:{1}\r\n异常堆栈:{2}\r\nCLR即将退出:{3}", ex.GetType(), ex.Message, ex.StackTrace, e.IsTerminating));
    }
     
    static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
    {
        Exception ex = e.Exception;
        MessageBox.Show(string.Format("捕获到未处理异常:{0}\r\n异常信息:{1}\r\n异常堆栈:{2}", ex.GetType(), ex.Message, ex.StackTrace));
    }
}

 

我们来试一下,运行程序,单击button2:

 

漂亮,监听到事件了。但在这里我们需要注意的是注意这个AppDomain.CurrentDomain.UnhandledException事件有一个事件参数叫UnhandledExceptionEventArgs e,它有一个bool类型的IsTerminating属性。这个属性指示了公共语言运行时(CLR)会不会因为本次异常而退出。如果这个属性是true的话,那么我们可以称这个错误是不可挽回的,就算我们监听到了这个事件,在这个事件的代码执行结束后,整个程序还是会崩溃退出的(因为我在VS里,所以被VS捕捉到了。)。

 

四、Web程序全局异常捕获

之前提到的都是Windows程序的全局异常捕获,现在Web程序也是我们经常要与之打交道的一部分。那Web程序该如何进行全局异常捕获呢?,下面以Asp.net MVC4做演示。

我们假设我们有一个Home控制器(Controller),里面有一个Error页面,访问这个页面将抛出一个异常。

public class HomeController : Controller
{
    public ActionResult Error()
    {
        throw new Exception();
    }
}

 

1. Global.asax中编写Application_Error函数

我们可以在Global.asax里写一个protected void Application_Error(object sender, EventArgs e)的函数,当发生未经处理的异常时,Asp.net将自动执行此函数。

在Global.asax文件的MvcApplication类里其他空白位置写上以下代码:

protected void Application_Error(object sender, EventArgs e)
{
    //获取到HttpUnhandledException异常,这个异常包含一个实际出现的异常
    Exception ex = Server.GetLastError();
    //实际发生的异常
    Exception innerException = ex.InnerException;
    if (innerException != null) ex = innerException;
     
    using (StreamWriter sw = new StreamWriter(@"C:\Log.txt"true))
    {
        sw.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
        sw.WriteLine("Global捕获到未处理异常:" + ex.GetType().ToString());
        sw.WriteLine("异常信息:" + ex.Message);
        sw.WriteLine("异常堆栈:" + ex.StackTrace);
        sw.WriteLine();
    }
     
    HttpContext.Current.Response.Write(string.Format("捕捉到未处理的异常:{0}<br/>", ex.GetType().ToString()));
    HttpContext.Current.Response.Write("Global已进行错误处理。");
     
    Server.ClearError();
}

 

实际运行情况:

 

2.使用Filter进行全局异常捕获

Filter其实就是过滤器,在Asp.net MVC中Filter一共分为4大类

分类 接口 默认实现 运行时间
Authorization IAuthorizationFilter AuthorizeAttribute 在Action方法之前和其它类型的Filter之前运行。
Action IActionFilter ActionFilterAttribute 在Action方法之前运行。
Result IResultFilter ActionFilterAttribute 在处理ActionResult之前或之后运行。
Exception IExceptionFilter HandleErrorAttribute 在Action方法、ActionResult和其他类型的Filter抛出异常时运行。

 

我们可以看到有一个Exception类型的Filter,我们在这里就是借助这个Exception的Filter进行全局异常捕获。

新建一个类,名叫MyExceptionFilter,使其继承FilterAttribute类,并实现IexceptionFilter接口。MyExceptionHandler类代码如下:

using System;
using System.IO;
using System.Web.Mvc;
 
namespace MvcApplication2
{
    public class MyExceptionHandler : FilterAttribute, IExceptionFilter
    {
        public void OnException(ExceptionContext filterContext)
        {
            Exception ex = filterContext.Exception as Exception;
            if (ex != null)
            {
                filterContext.Controller.ViewBag.UrlRefer = filterContext.HttpContext.Request.UrlReferrer;
                 
                using (StreamWriter sw = new StreamWriter(@"C:\Log.txt"true))
                {
                    sw.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
                    sw.WriteLine("Filter捕获到未处理异常:" + ex.GetType().ToString());
                    sw.WriteLine("异常信息:" + ex.Message);
                    sw.WriteLine("异常堆栈:" + ex.StackTrace);
                    sw.WriteLine();
                }
         
                filterContext.HttpContext.Response.Write(string.Format("捕捉到未处理的异常:{0}<br/>", ex.GetType().ToString()));
                filterContext.HttpContext.Response.Write("Filter已进行错误处理。");
            }
 
            filterContext.ExceptionHandled = true;//设置异常已经处理
        }
    }
}

 

然后我们打开项目的“App_Start”文件夹下的“FilterConfig.cs”文件,修改其RegisterGlobalFilters方法内的代码,修改如下:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new MyExceptionHandler(), 1);//自定义的验证特性
    filters.Add(new HandleErrorAttribute(), 2);
}

 

启动Web程序查看效果:

C# 全局异常捕获

开发界有那么一个笑话,说是“「我爱你」三个字,讲出来只要三秒钟,解释要三小时,证明却要一辈子。「Bug」三个字母,发现需要三秒,找到需要三小时,Debug却要一辈子。”。就算是资深的程序员也会写出Bug,但Bug并不可怕,重要的是在Bug发生的时候迅速定位Bug。

在Visual Studio中调试的时候,我们可以借助VS的调试工具进行调试,一旦出现未处理的异常时,VS也会在第一时间捕获并显示出来。随着开发的进行,终于程序要打包上线了。那么在上线之出了BUG我们该如何处理呢?

 

相信如果各位年龄够大,应该都见识过QQ出错崩溃吧?在零几年的时候QQ崩溃还不是一件稀罕事儿。每当QQ崩溃的时候都会弹出一个BUGReporter程序,会希望我们将出错的报告发送给腾讯

 

其实我们标题所说的全局异常捕获主要目标并不是为了将异常处理掉防止程序崩溃。因为当错误被你的全局异常捕获器抓到的时候,已经证实了你程序中存在BUG。一般而言,我们的全局异常捕获主要作用就是接收到异常之后进行异常的反馈。

 

一、简单粗暴:在Program.cs使用Try...Catch...

大家都知道,异常是通过Throw命令抛出,一路从抛出的模块里上抛,如果中途没有被try...catch...抓住的话就会一直抛到CLR(公共语言运行时)。如果用栈来描述这个过程的话,那就是异常会从栈的栈顶一路下沉,直到中途被try...catch...抓住或者直至沉到栈底,被CLR接住。CLR接收到异常之后的处理方式非常的简单粗暴——直接报错,然后关闭程序。大概表现就是这样:

 

这样:

 

还有这样:

 

不过根据刚刚我们所描述的异常上抛过程,我们不难发现:只要我们在程序把异常抛给CLR之前,抢先把异常捕获,那就可以做到全局异常处理了。不过这个try...catch...就必须放在栈的最下方。程序运行时栈的最下方函数其实就是程序运行时第一个调用的函数——main()函数。

比如说我有这么一个Windows窗体程序,一个Program.cs类,一个FrmMain.cs窗口。

Program.cs类内容如下:

static class Program
{
    /// <summary>
    /// 应用程序的主入口点。
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new FrmMain());
    }
}

 

FrmMain里有一个Button,其点击事件如下:

private void button1_Click(object sender, EventArgs e)
{
    throw new Exception();
}

 

如果就现在这样的代码而言,只要在运行时单击这个Button就能时整个程序报错崩溃。

 

现在我们改造Program.cs里的main()函数,改成以下内容:

/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main()
{
    try
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new FrmMain());
    }
    catch (Exception ex)
    {
        MessageBox.Show(string.Format("捕获到未处理异常:{0}\r\n异常信息:{1}\r\n异常堆栈:{2}", ex.GetType(), ex.Message, ex.StackTrace));
    }
}

 

现在再次执行程序,单击button1,我们可以看到现在这次的这个异常被main()函数中的try...catch...所抓获了。

 

我们在这里catch内只是放了一个MessageBox,后面可以根据自己的需要改成错误报告代码。

 

二、更优雅的事件监听:Application.ThreadException

刚刚在上面提到了一种简单粗暴的方法,就是用try...catch...把main()函数所有内容全部包住。不过这样的代码看起来就有点蠢了。有没有什么更加优雅的方法吗?

答案当然是有。还记得Application类吧,这个负责控制整个Windows 程序的运行。我们可能用到这个类的时候更多的时候用的是下面几个方法或属性:

 

今天在这里我们要用到一个Application类的一个事件:ThreadException。

我们可以按F12转到Application类的定义,在ThreadException事件上,微软对它的注释是:“在发生未捕获线程异常时发生。”,这正是我们的目标。

 

假设还是之前的那个程序,我们将程序的Program.cs内容填入以下代码:

static class Program
{
    /// <summary>
    /// 应用程序的主入口点。
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.ThreadException += Application_ThreadException;
         
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new FrmMain());
    }
 
    static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
    {
        Exception ex = e.Exception;
        MessageBox.Show(string.Format("捕获到未处理异常:{0}\r\n异常信息:{1}\r\n异常堆栈:{2}", ex.GetType(), ex.Message, ex.StackTrace));
    }
}

 

现在我们重新运行这个程序,单击button1,查看效果:

 

可以说效果出乎意料的完美,上一章里在main()函数里用try..catch...包住所有代码,当程序异常时将代码将会进入catch块里,处理完成后程序就退出了。然而在这边我们用Application.ThreadException事件监听并处理后,程序并不会因为异常而退出。可以说是可挽回的异常。

 

三、子线程异常捕获AppDomain.CurrentDomain.UnhandledException

在上面我们提到了可以通过监听Application.ThreadException事件来捕获程序异常,但这个Application.ThreadException事件只能捕获程序主线程上发生的异常。如果你用到了多线程,而且在子线程中发生了异常,不会触发Application.ThreadException事件的。

如果要监听子线程的异常,我们就需要再注册一个事件:AppDomain.CurrentDomain.UnhandledException

这个事件是在当前程序域内发生未处理异常时才会发生(如果没有监听Application.ThreadException事件的话,主线程异常最终也会触发这个事件)

我们在第一章的窗口里再加一个Button按钮,名为button2,其单击事件内容为:

private void button2_Click(object sender, EventArgs e)
{
    Thread thread = new Thread(() =>
    {
        throw new Exception();
    });
    thread.Start();
}

 

上面的代码会在运行时创建一个线程,线程代码只有一句“throw new Exception();”致使线程抛出异常。

我们再改一下Program.cs类的内容:

static class Program
{
    /// <summary>
    /// 应用程序的主入口点。
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.ThreadException += Application_ThreadException;
        AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
         
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new FrmMain());
    }
     
    static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
    {
        Exception ex = e.ExceptionObject as Exception;
        MessageBox.Show(string.Format("捕获到未处理异常:{0}\r\n异常信息:{1}\r\n异常堆栈:{2}\r\nCLR即将退出:{3}", ex.GetType(), ex.Message, ex.StackTrace, e.IsTerminating));
    }
     
    static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
    {
        Exception ex = e.Exception;
        MessageBox.Show(string.Format("捕获到未处理异常:{0}\r\n异常信息:{1}\r\n异常堆栈:{2}", ex.GetType(), ex.Message, ex.StackTrace));
    }
}

 

我们来试一下,运行程序,单击button2:

 

漂亮,监听到事件了。但在这里我们需要注意的是注意这个AppDomain.CurrentDomain.UnhandledException事件有一个事件参数叫UnhandledExceptionEventArgs e,它有一个bool类型的IsTerminating属性。这个属性指示了公共语言运行时(CLR)会不会因为本次异常而退出。如果这个属性是true的话,那么我们可以称这个错误是不可挽回的,就算我们监听到了这个事件,在这个事件的代码执行结束后,整个程序还是会崩溃退出的(因为我在VS里,所以被VS捕捉到了。)。

 

四、Web程序全局异常捕获

之前提到的都是Windows程序的全局异常捕获,现在Web程序也是我们经常要与之打交道的一部分。那Web程序该如何进行全局异常捕获呢?,下面以Asp.net MVC4做演示。

我们假设我们有一个Home控制器(Controller),里面有一个Error页面,访问这个页面将抛出一个异常。

public class HomeController : Controller
{
    public ActionResult Error()
    {
        throw new Exception();
    }
}

 

1. Global.asax中编写Application_Error函数

我们可以在Global.asax里写一个protected void Application_Error(object sender, EventArgs e)的函数,当发生未经处理的异常时,Asp.net将自动执行此函数。

在Global.asax文件的MvcApplication类里其他空白位置写上以下代码:

protected void Application_Error(object sender, EventArgs e)
{
    //获取到HttpUnhandledException异常,这个异常包含一个实际出现的异常
    Exception ex = Server.GetLastError();
    //实际发生的异常
    Exception innerException = ex.InnerException;
    if (innerException != null) ex = innerException;
     
    using (StreamWriter sw = new StreamWriter(@"C:\Log.txt"true))
    {
        sw.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
        sw.WriteLine("Global捕获到未处理异常:" + ex.GetType().ToString());
        sw.WriteLine("异常信息:" + ex.Message);
        sw.WriteLine("异常堆栈:" + ex.StackTrace);
        sw.WriteLine();
    }
     
    HttpContext.Current.Response.Write(string.Format("捕捉到未处理的异常:{0}<br/>", ex.GetType().ToString()));
    HttpContext.Current.Response.Write("Global已进行错误处理。");
     
    Server.ClearError();
}

 

实际运行情况:

 

2.使用Filter进行全局异常捕获

Filter其实就是过滤器,在Asp.net MVC中Filter一共分为4大类

分类 接口 默认实现 运行时间
Authorization IAuthorizationFilter AuthorizeAttribute 在Action方法之前和其它类型的Filter之前运行。
Action IActionFilter ActionFilterAttribute 在Action方法之前运行。
Result IResultFilter ActionFilterAttribute 在处理ActionResult之前或之后运行。
Exception IExceptionFilter HandleErrorAttribute 在Action方法、ActionResult和其他类型的Filter抛出异常时运行。

 

我们可以看到有一个Exception类型的Filter,我们在这里就是借助这个Exception的Filter进行全局异常捕获。

新建一个类,名叫MyExceptionFilter,使其继承FilterAttribute类,并实现IexceptionFilter接口。MyExceptionHandler类代码如下:

using System;
using System.IO;
using System.Web.Mvc;
 
namespace MvcApplication2
{
    public class MyExceptionHandler : FilterAttribute, IExceptionFilter
    {
        public void OnException(ExceptionContext filterContext)
        {
            Exception ex = filterContext.Exception as Exception;
            if (ex != null)
            {
                filterContext.Controller.ViewBag.UrlRefer = filterContext.HttpContext.Request.UrlReferrer;
                 
                using (StreamWriter sw = new StreamWriter(@"C:\Log.txt"true))
                {
                    sw.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
                    sw.WriteLine("Filter捕获到未处理异常:" + ex.GetType().ToString());
                    sw.WriteLine("异常信息:" + ex.Message);
                    sw.WriteLine("异常堆栈:" + ex.StackTrace);
                    sw.WriteLine();
                }
         
                filterContext.HttpContext.Response.Write(string.Format("捕捉到未处理的异常:{0}<br/>", ex.GetType().ToString()));
                filterContext.HttpContext.Response.Write("Filter已进行错误处理。");
            }
 
            filterContext.ExceptionHandled = true;//设置异常已经处理
        }
    }
}

 

然后我们打开项目的“App_Start”文件夹下的“FilterConfig.cs”文件,修改其RegisterGlobalFilters方法内的代码,修改如下:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new MyExceptionHandler(), 1);//自定义的验证特性
    filters.Add(new HandleErrorAttribute(), 2);
}

 

启动Web程序查看效果:

 

写在最后:

在Windows程序里,如果同时监听了Application.ThreadException事件和AppDomain.CurrentDomain.UnhandledException事件的话,则异常优先被Application.ThreadException事件捕获。但Application.ThreadException事件只能监听程序主线程抛出的异常。

在Web程序里,如果同时使用了Global.asax中编写Application_Error函数进行全局异常捕获和使用Filter进行全局异常捕获,异常优先被Filter捕获。

 

爱自己爱生活