[C#] 淺談反射

入鄉隨俗的配圖

在.NET中,反射(reflection)是一個運行庫類型發現的過程。使用反射服務,可以通過編程使用一個友好的對象模型得到與通過ildasm.exe顯示的相同元數據信息。

這段話引用自《精通C#》,通俗地講就是,通過反射我們能在運行時獲得程序或程序集所包含的所有類型的列表以及給定類型定義的方法、字段、屬性和事件;還可以動態發現一組給定類型支持的接口、方法的參數和其他相關細節。

1. 反射的一個最直觀的用法

我曾經在開發的時候遇到這樣一個例子:在一個類A中增加了一些field,但是它們暫時不暴露給創建和修改類A的方法。但是又要保證這些field是可以通過專門的API來修改,所以要對每個field增加一對API來進行查看和修改。

然后我就寫啊寫啊,發現怎么這些API都是做的相同的事情,唯一不同的是函數名和訪問的字段名。
所以,我就想,能不能只寫一個函數來搞定這些事情呢?
于是就想到了反射,通過傳進來參數表示要修改的字段名,然后動態地獲取到該字段,并對它進行賦值或者讀取它的值。

下面是一個簡單的實例:

namespace ReflectionEx1
{
    public class ReflectionEx1
    {
        public int fieldInt { get; set; }
        public double fieldDouble { get; set; }
        public string fieldString { get; set; }
    }

    class Program
    {
        public static string GetFieldValue(string FieldName, object obj)
        {
            try
            {
                var property = obj.GetType().GetProperty(FieldName);
                if (property != null)
                {
                    object o = property.GetValue(obj, null);
                    return Convert.ToString(o);
                }

                Console.WriteLine("{0} does not have property {1}", obj.GetType(), FieldName);
                return null;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                return null;
            }
        }

        public static bool SetFieldValue(string FieldName, string Value, object obj)
        {
            try
            {
                var property = obj.GetType().GetProperty(FieldName);
                if (property != null)
                {
                    object v = Convert.ChangeType(Value, property.PropertyType);
                    property.SetValue(obj, v, null);
                    return true;
                }

                Console.WriteLine("{0} does not have property {1}", obj.GetType(), FieldName);
                return false;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                return false;
            }
        }       

        static void Main(string[] args)
        {
            var testClass = new ReflectionEx1();
            if (SetFieldValue("fieldInt", "1", testClass))
            {
                Console.WriteLine(GetFieldValue("fieldInt", testClass));
            }

            if (SetFieldValue("fieldDouble", "2.5", testClass))
            {
                Console.WriteLine(GetFieldValue("fieldDouble", testClass));
            }

            if (SetFieldValue("fieldString", "test", testClass))
            {
                Console.WriteLine(GetFieldValue("fieldString", testClass));
            }
        }
    }
}

這里所有的值都是通過string的方式進行轉換的,所以在用SetFieldValue方法時傳入的值是統一的string類型。這樣在對某個域進行賦值時完全是看這個域的類型,然后嘗試把string轉成那個類型。如果我們外面調用的時候傳入了錯誤的類型呢?

所以下面又寫了另外一個版本,通過泛型來支持不同的類型,而不是通過string來轉換。這樣,當你試圖用stringfieldInt進行賦值時會出現下面的異常:

Object of type 'System.String' cannot be converted to type 'System.Int32'.

namespace ReflectionEx1
{
    public class ReflectionEx1
    {
        public int fieldInt { get; set; }
        public double fieldDouble { get; set; }
        public string fieldString { get; set; }
    }

    class Program
    {
        public static T GetFieldValue<T>(string FieldName, object obj)
        {
            try
            {
                var property = obj.GetType().GetProperty(FieldName);
                if (property != null)
                {
                    return (T)property.GetValue(obj, null);
                }

                Console.WriteLine("{0} does not have property {1}", obj.GetType(), FieldName);
                return default(T);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                return default(T);
            }
        }

        public static bool SetFieldValue<T>(string FieldName, T Value, object obj)
        {
            try
            {
                var property = obj.GetType().GetProperty(FieldName);
                if (property != null)
                {
                    property.SetValue(obj, Value, null);
                    return true;
                }

                Console.WriteLine("{0} does not have property {1}", obj.GetType(), FieldName);
                return false;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                return false;
            }
        }       

        static void Main(string[] args)
        {
            var testClass = new ReflectionEx1();
            if (SetFieldValue("fieldInt", 1, testClass))
            {
                Console.WriteLine(GetFieldValue<int>("fieldInt", testClass));
            }

            if (SetFieldValue("fieldDouble", 2.5, testClass))
            {
                Console.WriteLine(GetFieldValue<double>("fieldDouble", testClass));
            }

            if (SetFieldValue("fieldString", "test", testClass))
            {
                Console.WriteLine(GetFieldValue<string>("fieldString", testClass));
            }
        }
    }
}

不過,遺憾的是我最后并沒有使用反射。因為這些暴露出來的API與類A是分離的東西,使用API的人并不是十分清楚類A的結構。也就是說你可以告訴用戶,這個API是修改field1的,但是你不能妄想他們清楚地知道field1的名字(包括大小寫)。這里看起來有點不合乎邏輯,既然他們知道是修改field1,為什么又不知道field1。舉個例子,假設有一個布爾值field1,它被用來控制類A是否具有功能x。那么我們可以給用戶提供一個API叫做EnableX(),這個API做的事情就是將field1設置為true(這個是不必暴露給用戶的)。

2. 構建自定義的元數據查看器

上面的一個簡單的例子可以看到我們可以在運行的時候通過反射,利用property的名字獲取它。下面我們將自定義一個元數據查看器,通過這個例子來看看反射還能做到什么。

(1)反射方法

我們可以通過Type類的GetMethods()函數得到一個MethodInfo[]。下面我們利用它寫一個ListMethods()方法,來輸出傳入類型的所有方法名。傳入的類型可以是C#的內置類型(值類型和引用類型都行),也可以是自定義的類型。而調用ListMethods()方法時傳的參數可以是利用對象的GetType()函數,也可以使用typeof運算符。

using System.Reflection;

namespace MyTypeViewer
{
    struct TestStruct
    {
        public void Func3() { }
    }

    public class TestClass
    {
        public void Func1() { }
        private void Func2() { }
    }

    class Program
    {
        static void ListMethods(Type type)
        {
            Console.WriteLine("***** Methods *****");
            var methodNames = from m in type.GetMethods() select m.Name;
            foreach (var name in methodNames)
            {
                Console.WriteLine("-> {0}", name);
            }
            Console.WriteLine();
        }

        static void Main(string[] args)
        {
            var test = new TestClass();
            ListMethods(test.GetType());
            ListMethods(typeof(TestStruct));
            ListMethods(typeof(string));
        }
    }
}

下面的運行結果中由于string類的方法太多,所以省略了部分沒有顯示。

***** Methods *****
-> Func1
-> ToString
-> Equals
-> GetHashCode
-> GetType

***** Methods *****
-> Func3
-> Equals
-> GetHashCode
-> ToString
-> GetType

***** Methods *****
// ignore some methods
-> CopyTo
-> ToCharArray
-> IsNullOrEmpty
-> IsNullOrWhiteSpace
-> Split
-> Substring

這里或許你會有一個疑問,不是獲取所有的方法嗎?為什么TestClass.Func2沒有顯示呢?這是因為Func2是私有方法,而GetMethods()方法還有一個帶參數的重寫方法,參數為BindingFlags 枚舉

public abstract MethodInfo[] GetMethods(    
      BindingFlags bindingAttr
)

所以要讀取私有的(private或者protected)方法,你需要在調用GetMethods()時傳入參數BindingFlags.NonPublic | BindingFlags.Instance,這樣上例的方法除了輸出Func2之外,還會輸出下面兩個方法。這兩個方法是Object類中的protected方法,如果不想獲取它們,可以在上面的參數中再添加BindingFlags.DeclaredOnly

-> Finalize
-> MemberwiseClone
(2)反射字段和屬性

運行時獲取字段和屬性的方式跟方法類似,采用GetFileds()函數。GetFileds()方法返回當前 Type的所有公共字段;GetFields(BindingFlags)方法則可以使用指定綁定約束,搜索當前 Type定義的字段, 它們返回的結果都是FieldInfo[]類型。

namespace MyTypeViewer
{
    public class TestClass
    {
        public int field1;
        private string field2;
        protected double field3;
        public string property1 { get; set; }
        
        public void Func1() { }
        protected void Func2() { }
        private void Func3() { }
    }

class Program
    {
        static void Display(string title, IEnumerable<string> names)
        {
            Console.WriteLine("***** {0} *****", title);
            foreach (var name in names)
            {
                Console.WriteLine("-> {0}", name);
            }
            Console.WriteLine();
        }

        static void ListMethods(Type type)
        {
            var methodNames = from m in type.GetMethods() select m.Name;
            Display("Methods", methodNames);
        }

        static void ListFields(Type type)
        {
            var fieldNames = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Select(f => f.Name);
            Display("Fields", fieldNames);
        }

        static void ListProperties(Type type)
        {
            var propertyNames = type.GetProperties().Select(f => f.Name);
            Display("Properties", propertyNames);
        }

        static void Main(string[] args)
        {
            ListFields(typeof(TestClass));
        }
    }
}

輸出結果:

***** Fields *****
-> field1
-> field2
-> field3
-> <property1>k__BackingField

***** Properties *****
-> property1

GetFields()的返回結果中我們可以看到<property1>k__BackingField,但是要真正獲取property1的話需要采用GetProperties()方法。

(3)反射實現的接口

反射實現的接口也是類似的情況,采用Type類的GetInterfaces()方法,它返回的是Type[]

namespace MyTypeViewer
{
    interface ISampleInterface
    {
        void SampleMethod();
    }

    public class TestClass : ISampleInterface
    {
        void ISampleInterface.SampleMethod()
        {
            Console.WriteLine("implement interface member");
        }
    }

    class Program
    {
        static void Display(string title, IEnumerable<string> names)
        {
            Console.WriteLine("***** {0} *****", title);
            foreach (var name in names)
            {
                Console.WriteLine("-> {0}", name);
            }
            Console.WriteLine();
        }

        static void ListInterfaces(Type type)
        {
            var interfaces = type.GetInterfaces().Select(t => t.Name);
            Display("Interfaces", interfaces);
        }

        static void Main(string[] args)
        {
            ListInterfaces(typeof(TestClass));
        }
    }
}

輸出結果:

***** Interfaces *****
-> ISampleInterface
(4)反射泛型類型

上面的例子中我們都采用typeof操作符獲取某種類型,對于泛型類型,則必須指定T,如ListMethods(typeof(List<string>));是可以的,但是不能直接使用typeof(List)

另外我們還可以通過Type.GetType(typeName)方法來獲取某種類型,如Type t = Type.GetType("System.Int32");。這里必須使用完全限定名,而且要求大小寫完全匹配。如果使用"System.int32"或者"Int32"會找不到我們想要的類型,t為空。

而對于泛型類型,若我們采用上述方法來獲取類型,則需要使用反勾號(`)加上數字值的語法來表示類型支持的參數個數。
例如,System.Collections.Generic.List<T>可以寫成"System.Collections.Generic.List\1"; 而System.Collections.Generic.Dictionary<TKey, TValue>可以寫成"System.Collections.Generic.Dictionary`2"`。

(5)反射方法參數和返回值

上面的所有示例中,我們都只返回了名字,其實我們還可以從MethodInfo或者FieldInfo或者Type中獲取更多其他的信息。
下面我們以MethodInfo為例,來豐富上面的ListMethods()方法,除了列出所有方法的名字外,還輸出它們的參數(MethodInfo.GetParameters())和返回值(MethodInfo.ReturnType)。

static void ListMethods(Type type)
{
    var methods = type.GetMethods();
    Console.WriteLine("***** Methods *****");
    foreach (var m in methods)
    {
        string retVal = m.ReturnType.FullName;
        var paramInfos = m.GetParameters().Select(p => string.Format("{0} {1}", p.ParameterType, p.Name));
        string paramInfo = String.Join(", ", paramInfos.ToArray());
        Console.WriteLine("-> {0} {1}( {2} )", retVal, m.Name, paramInfo);
    }
}

輸出結果:

***** Methods *****
-> System.String get_property1(  )
-> System.Void set_property1( System.String value )
-> System.String Func1( System.Int32 p1, System.Double p2 )
-> System.String ToString(  )
-> System.Boolean Equals( System.Object obj )
-> System.Int32 GetHashCode(  )
-> System.Type GetType(  )

3. 自定義的元數據查看器有什么用

上面我們通過幾個例子可以看到Type類型有很多功能很強大的方法可以查看元數據。那么,查看這些元數據有什么用呢?當你想動態使用這些類型時,它們就變得非常有用了,下面通過幾個常用的例子來說明如何使用。

(1)如何動態創建對象

動態創建對象可以使用Assembly.CreateInstance()Activator.CreateInstance()方法。動態創建出來的對象與new創建的對象一樣,只是前者不需要在編譯前就知道類的名字。

namespace MyTypeViewer
{
    public class TestClass
    {
        public string property1 { get; set; }
        
        public void Func1() 
        {
            Console.WriteLine("Hello World!");
        }
    }

    public static class ReflectionHelper
    {
        public static T CreateInstance<T>(string assemblyName, string nameSpace, string className)
        {
            try
            {
                string fullName = string.Fortmat("{0}.{1}", nameSpace, className);
                object obj = Assembly.Load(assemblyName).CreateInstance(fullName);
                return (T)obj;
            }
            catch
            {
                return default(T);
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var testInstance = ReflectionHelper.CreateInstance<TestClass>("MyTypeViewer", "MyTypeViewer", "TestClass");
            testInstance.property1 = "Test Property";
            testInstance.Func1();
        }
    }
}
(2)如何動態創建委托

CreateDelegate有很多種不同的重載方法,我們這里采用的是用MethodInfo

public static Delegate CreateDelegate(  
        Type type, 
        object firstArgument,   
        MethodInfo method
)

下面的例子來自MSDN

namespace CreateDelegate
{
    class ExampleForm : Form
    {
        public ExampleForm()
            : base()
        {
            this.Text = "Click me";
        }
    }

    class Example
    {
        public static void Main()
        {
            Example ex = new Example();
            ex.HookUpDelegate();
        }

        private void HookUpDelegate()
        {
            Assembly assem = typeof(Example).Assembly;

            Type tExForm = assem.GetType("CreateDelegate.ExampleForm");
            Object exFormAsObj = Activator.CreateInstance(tExForm);

            EventInfo evClick = tExForm.GetEvent("Click");
            Type tDelegate = evClick.EventHandlerType;

            MethodInfo miHandler =
                typeof(Example).GetMethod("LuckyHandler",
                    BindingFlags.NonPublic | BindingFlags.Instance);

            Delegate d = Delegate.CreateDelegate(tDelegate, this, miHandler);

            MethodInfo addHandler = evClick.GetAddMethod();
            Object[] addHandlerArgs = { d };
            addHandler.Invoke(exFormAsObj, addHandlerArgs);

            Application.Run((Form)exFormAsObj);
        }

        private void LuckyHandler(Object sender, EventArgs e)
        {
            MessageBox.Show("This event handler just happened to be lying around.");
        }
    }
}

這里我們先動態創建了一個ExampleForm窗體實例,然后取得它上面的Click事件的委托類型tDelegate,再根據該委托類型和我們的LuckyHandler方法創建委托d。最后,運行該程序時會彈出一個窗體,點擊任意位置就會調用LuckyHandler方法。

4. 反射的應用實例1

為了更好地理解反射的機制,下面我們通過一個簡單的動態加載插件的實例來說明反射的作用。
(1)首先,我們建立一個簡單的ClassLibrary來定義我們接下來要用到的插件的接口,編譯下面的這個接口我們將得到CommonTypeForPlugIn.dll (里面定義了一個接口IFunctionality和它的一個DoIt()函數,以及一個屬性類CompanyInfoAttribute)。

namespace CommonTypeForPlugIn
{
    public interface IFunctionality
    {
        void DoIt();
    }

    [AttributeUsage(AttributeTargets.Class)]
    public sealed class CompanyInfoAttribute : Attribute
    {
        public string CompanyName { get; set; }
        public string CompanyUrl { get; set; }
    }
}

(2)在CommonTypeForPlugIn的基礎上實現不同的插件,這里提供了Demo1Demo2

using CommonTypeForPlugIn; // You should add CommonTypeForPlugIn.dll to References first

namespace Demo1
{
    [CompanyInfo(CompanyName = "Demo1", CompanyUrl = "http://demo1.com")]
    public class Demo1 : IFunctionality
    {
        void IFunctionality.DoIt()
        {
            Console.WriteLine("Demo1 is in using now.");
        }
    }
}
using CommonTypeForPlugIn;

namespace Demo2
{
    [CompanyInfo(CompanyName = "Demo2", CompanyUrl = "http://demo2.com")]
    public class Demo2 : IFunctionality
    {
        void IFunctionality.DoIt()
        {
            Console.WriteLine("Demo2 is in using now.");
        }
    }
}

(3)接下來我們創建一個應用臺程序來實現動態加載上面不同的插件。
PlugInExample類里我們提供了DisplayPlugInsForCommonType()DisplayAndRunPlugIn()兩個公共的函數用來分別顯示所有的實現CommonType的插件(這里我們上面編譯生成的dll都拷貝到這個控制臺程序的objd/debug/目錄下新建的plugins文件夾下了)和運行一個插件。
DisplayAndRunPlugIn(fileName)函數根據用戶輸入的插件名去加載那個dll,然后動態給插件里的實現IFunctionality接口的類創建實例,并執行它的DoIt()方法,最后動態獲取插件里的CompanyInfoAttribute類,并輸出其中的特性值 DisplayCompanyInfo(t)

using System.IO;
using CommonTypeForPlugIn;
using System.Reflection;

namespace LearnCSharp
{
    public class PlugInExample
    {
        public void DisplayPlugInsForCommonType()
        {
            var plugins = ListAllExternalModule();
            if (plugins == null || !plugins.Any())
            {
                Console.WriteLine("No plugin provided.");
            }
            else
            {
                Console.WriteLine("List of all plugins:");
                foreach (var plugin in plugins)
                {
                    Console.WriteLine(Path.GetFileName(plugin));
                }
            }
        }

        public void DisplayAndRunPlugIn(string fileName)
        {
            if (fileName == "CommonTypeForPlugIn.dll")
            {
                Console.WriteLine("Dll {0} has no plug-in.", fileName);
            }
            else if (!LoadExternalModule(fileName))
            {
                Console.WriteLine("Load Dll {0} failed.", fileName);
            }
        }

        private IEnumerable<string> ListAllExternalModule()
        {
            var directory = string.Format("{0}/plugins/", Directory.GetCurrentDirectory().TrimEnd('/'));
            return Directory.GetFiles(directory, "*.dll");
        }

        private bool LoadExternalModule(string fileName)
        {
            Assembly plugInAsm = null;
            var path = string.Format("{0}/plugins/{1}", Directory.GetCurrentDirectory().TrimEnd('/'), fileName);
            
            try
            {
                plugInAsm = Assembly.LoadFrom(path);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Failed to load assembly {0}, error: {1}", path, ex.Message);
                return false;
            }

            var classTypes = plugInAsm.GetTypes().Where(t => t.IsClass && t.GetInterface("IFunctionality") != null);
            foreach (Type t in classTypes)
            {
                IFunctionality func = (IFunctionality)plugInAsm.CreateInstance(t.FullName, true);
                func.DoIt();
                DisplayCompanyInfo(t);
            }
            return true;
        }

        private void DisplayCompanyInfo(Type t)
        {
            var companyInfos = t.GetCustomAttributes(false).Where(c => c.GetType() == typeof(CompanyInfoAttribute));
            foreach (CompanyInfoAttribute companyInfo in companyInfos)
            {
                Console.WriteLine("CompanyName: {0}, CompanyUrl: {1}", companyInfo.CompanyName, companyInfo.CompanyUrl);
            }
        }
    }   

    class Program
    {
        static void Main(string[] args)
        {
            var ex = new PlugInExample();
            ex.DisplayPlugInsForCommonType();
            Console.WriteLine("Please choose a dll to load:");
            string fileName = Console.ReadLine();
            ex.DisplayAndRunPlugIn(fileName);
        }
    }
}

輸出結果:

List of all plugins:
Demo1.dll
Demo2.dll
Please choose a dll to load:
Demo1.dll
Demo1 is in using now.
CompanyName: Demo1, CompanyUrl: http://demo1.com

參考文獻:

  1. .NET Framework 中的反射
  2. C#反射設置屬性值和獲取屬性值
  3. 《精通C#》
  4. C#Reflection學習記錄
  5. [整理]C#反射(Reflection)詳解
  6. C#反射之創建對象實例
  7. C#高效反射調用方法類
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,807評論 18 139
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,726評論 18 399
  • 哈哈哈哈
    小呆羊啦啦啦閱讀 197評論 0 0
  • 自我小學一年級起, 爸媽和我就搬去了離學校單程兩個小時的鄉下。所謂當時的鄉下也就是現在的交通路,公交車下來走半小時...
    閭得水閱讀 306評論 1 6
  • 黃瓜清脆,熏魚去了刺入口綿軟富含脂肪、蛋白質獨特的香味,而熱量極低,如果只放蔬菜,家里的男人和孩子肯定吃不上幾口,...
    孔雀東南飛飛閱讀 488評論 6 14