Spring框架學(xué)習(xí)之注解配置與AOP思想

?????上篇我們介紹了Spring中有關(guān)高級依賴關(guān)系配置的內(nèi)容,也可以調(diào)用任意方法的返回值作為屬性注入的值,它解決了Spring配置文件的動態(tài)性不足的缺點(diǎn)。而本篇,我們將介紹Spring的又一大核心思想,AOP,也就是面向切面編程。這是對面向?qū)ο缶幊痰囊粋€(gè)擴(kuò)展,即便問世不長,但是已經(jīng)成為當(dāng)下最流行的編程思想之一。本篇主要涉及以下內(nèi)容:

  • Spring中的后置處理器
  • "零配置"實(shí)現(xiàn)Bean的配置
  • Spring AOP

一、后置處理器
為了實(shí)現(xiàn)良好的擴(kuò)展性,Spring允許我們擴(kuò)展它的IOC容器,它提供了兩種后置處理器來支持我們對容器進(jìn)行擴(kuò)展。

  • Bean后置處理器:該處理器會對容器中的bean進(jìn)行增強(qiáng)
  • 容器后置處理器:該處理器針對容器,對容器進(jìn)行額外增強(qiáng)

1、Bean后置處理器
Bean后置處理器需要繼承接口BeanPostProcessor,并實(shí)現(xiàn)它的如下兩個(gè)方法:

  • public Object postProcessBeforeInitialization(Object o, String s):在當(dāng)前bean實(shí)例初始化屬性注入的之前回調(diào)
  • public Object postProcessAfterInitialization(Object o, String s):在當(dāng)前bean實(shí)例初始化屬性注入之后回調(diào)

當(dāng)整個(gè)容器在加載的時(shí)候,會掃描整個(gè)容器中的bean,如果發(fā)現(xiàn)有bean實(shí)現(xiàn)了接口BeanPostProcessor,那么就將該bean注冊為Bean后置處理器,一旦其他bean實(shí)例化完成之后,將逐個(gè)調(diào)用后置處理器進(jìn)行bean實(shí)例的增強(qiáng)。

在這兩個(gè)方法中,傳入了同樣的兩個(gè)參數(shù),第一個(gè)參數(shù)表示即將被后處理的bean實(shí)例,第二個(gè)參數(shù)是該實(shí)例在容器中的 id屬性。postProcessBeforeInitialization方法指明在容器創(chuàng)建實(shí)例之后,但是在實(shí)際初始化屬性之前回調(diào),此處傳過來的bean實(shí)例是一個(gè)完整的實(shí)例,它包含還未實(shí)際初始化的屬性的值信息,該方法的返回值就是最終保存在容器中的實(shí)例。

postProcessAfterInitialization方法是類似的,它會在容器初始化屬性結(jié)束后回調(diào),在該方法中,我們也可以修改該bean的信息,最終返回的bean將作為容器中真實(shí)存在的bean,對應(yīng)于傳入的該bean的引用,相當(dāng)于修改了該bean實(shí)例。

我們簡單看個(gè)例子:

//定義兩個(gè)類,并定義兩個(gè)屬性name和age
//容器中配置兩個(gè)bean實(shí)例
<bean id="programmer" class="Test_spring.Programmer" p:name="single" p:age="22" />

<bean id="coder" class="Test_spring.Coder" p:name="walker" p:age="21" />
//定義Bean后置處理器
public class MyBeanProcess implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object o, String s) throws BeansException {
        //屬性初始化之前
        if(o instanceof Programmer){
            Programmer programmer = (Programmer) o;
            programmer.setAge(50);
        }else if(o instanceof Coder) {
            Coder coder = (Coder) o;
            coder.setAge(60);
        }
        return o;
    }
    @Override
    public Object postProcessAfterInitialization(Object o, String s) throws BeansException {
        //屬性初始化之后
        if(o instanceof Programmer){
            Programmer programmer = (Programmer) o;
            System.out.println(programmer.getName() + "," + programmer.getAge());
        }else if(o instanceof Coder) {
            Coder coder = (Coder) o;
            System.out.println(coder.getName() + "," + coder.getAge());
        }
        return o;
    }
}
//將該Bean后置處理器實(shí)例配置在容器中
<bean class="Test_spring.MyBeanProcess" />

我們看最終的輸出結(jié)果:

這里寫圖片描述

程序很簡單,在實(shí)際初始化屬性之前,我們分別修改兩個(gè)bean實(shí)例的age屬性,又在屬性初始化結(jié)束時(shí),打印了他們的信息。Bean的后處理器一般就這么用,當(dāng)然此處的例子有點(diǎn)大材小用了,根據(jù)實(shí)際情況適時(shí)選擇使用即可。

2、容器后置處理器
Bean后置處理器負(fù)責(zé)增強(qiáng)處理所有的bean實(shí)例,而容器后置處理器則只負(fù)責(zé)處理容器本身。容器后置處理器必須實(shí)現(xiàn)接口 BeanFactoryPostProcessor,并實(shí)現(xiàn)其一個(gè)方法:

void postProcessBeanFactory(ConfigurableListableBeanFactory var1)

通過實(shí)現(xiàn)該方法體,我們可以做到對Spring容器進(jìn)行擴(kuò)展,通常來說,我們也很少自己去擴(kuò)展Spring容器,畢竟難度有點(diǎn)大。我們會使用Spring為我們內(nèi)置的容器后處理器,例如:屬性占位符配置器。

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
        <list>
            <!--多個(gè)屬性文件都可以一起進(jìn)行讀取-->
            <value>db.properties</value>
        </list>
    </property>
</bean>

<bean id="Dbcon" class="Test_spring.DbCon"
      p:driverClass="${jdbc.driverClassName}"
      p:conUrl="${jdbc.url}"
      p:userName="${jdbc.username}"
      p:pwd="${jdbc.password}"
/>

屬性文件:

//屬性文件名:db.properties
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///test
jdbc.username=root
jdbc.password=123456

上述代碼中,我們使用Spring為我們提供的一個(gè)屬性占位配置器,PropertyPlaceholderConfigurer。在實(shí)例化容器中的bean實(shí)例之前,容器會調(diào)用PropertyPlaceholderConfigurer容器后置處理器讀取指定的Properties文件并保存在Spring配置信息中。

于是,我們可以使用${....}來獲取被加載屬性文件中的內(nèi)容。

二、注解配置Bean實(shí)例
一直以來,我們都是使用的XML形式來配置我們的bean實(shí)例,但在潮流的推動下,大部分的Java框架也開始傾向于使用簡單的注解來配置我們的bean實(shí)例。Spring也已經(jīng)完全支持注解配置了。那么本小節(jié)就將學(xué)習(xí)下使用注解對bean實(shí)例的配置。

1、標(biāo)識Bean類
在XML中,一個(gè)bean元素代表一個(gè)bean實(shí)例,它的class屬性指定了它的類型,id指定了它的名稱。而在我們注解中,使用以下幾種注解來標(biāo)識Bean類:

  • @Component:標(biāo)識一個(gè)普通Bean
  • @Controller:標(biāo)識一個(gè)控制器組件
  • @Service:標(biāo)識一個(gè)業(yè)務(wù)組件
  • @Repository:標(biāo)識一個(gè)DAO組件

我們這里主要使用注解@Component來標(biāo)識Bean類,其他的三種類型的注解在整合第三方框架的時(shí)候再做詳細(xì)介紹。例如:

@Component("teacher")
public class Teacher {
    private String name;
    private int age;
    //省略setter方法
}

上述代碼等效于以下的XML配置:

<bean id="teacher" class="Test_spring.Teacher" />

當(dāng)然,如果想要Spring的注解生效,還需要在XML中配置掃描器:

<!--配置注解掃描器-->
<context:component-scan base-package="Test_spring" />

base-package屬性告訴Spring容器,應(yīng)該從哪個(gè)包開始掃描所有被Spring注解修飾的類。這樣我們就可以在容器外通過getBean獲取到該實(shí)例了。

Teacher teacher = (Teacher) context.getBean("teacher");

2、指定Bean實(shí)例的作用域
XML中指定bean作用域通常使用scope屬性來指定,例如:

<bean id="coder" class="Test_spring.Coder" scope="singleton" />

在Spring注解中,指定bean實(shí)例的作用域相對簡單很多。例如:

@Scope("singleton")
@Component(value = "teacher")
public class Teacher {
    ........
}

直接使用@Scope注解進(jìn)行作用域指定即可。

3、配置屬性依賴
Spring中,我們使用注解@Resources來給屬性注入依賴。例如:

//容器中配置coder的bean實(shí)例
<bean id="coder" class="Test_spring.Coder" p:name="single" p:age="22"/>
@Component(value = "teacher")
public class Teacher {
    private String name;
    private int age;
    private Coder coder;

    @Resource(name = "coder")
    public void setCoder(Coder coder) {
        this.coder = coder;
    }
    //省略其他setter方法
}

@Resource的name屬性指向的容器中已經(jīng)配置的bean實(shí)例id,它實(shí)現(xiàn)了和ref一樣的語義,需要特別注意的是,@Resource注解是修飾在setter方法上的,而非直接修飾實(shí)例屬性。

4、管理Bean實(shí)例的生命周期
在XML中,我們使用init-method和destory-method來管理bean的兩個(gè)特殊時(shí)間點(diǎn)。當(dāng)然,使用注解的話會清晰很多。例如:

@Component(value = "teacher")
public class Teacher {
    private String name;
    private int age;
    //省略setter方法

    @PostConstruct
    public void init(){
        System.out.println("initialize all properties...");
    }

    @PreDestroy
    public void destory(){
        System.out.println("destory this bean ....");
    }
}

我們使注解@PostConstruct標(biāo)識初始化屬性之后回調(diào)的方法,使用注解@PreDestroy指定在bean實(shí)例銷毀之前回調(diào)的方法。

常用的注解基本上就是這幾個(gè),還有一些例如@Autowire、@Qualifier用于指定依賴的自動裝配和精準(zhǔn)自動裝配的注解,由于本身使用場景有限,具體用到的時(shí)候,可以再次學(xué)習(xí)。

三、Spring AOP
AOP(Aspect Orient Programming),面向切面編程。它作為面向?qū)ο缶幊趟季S的一種延伸,逐漸成為了一種成熟的編程思維。AspectJ是當(dāng)下對AOP思想實(shí)現(xiàn)情況中最優(yōu)秀的框架,它提供了強(qiáng)大的AOP功能,有著自己的編譯器和織入器。目前而言,Spring有著自己的AOP實(shí)現(xiàn),底層是基于動態(tài)代理,但是也逐漸向AspectJ的規(guī)范靠近,并且在整個(gè)Spring AOP中使用的注解全部依賴AspectJ的注解,也就是說一旦哪天Spring 底層修改了AOP的實(shí)現(xiàn),采用AspectJ做實(shí)現(xiàn)的話,并不影響上層注解的使用。既然是基于動態(tài)代理的,那我們先簡單回顧下動態(tài)代理的內(nèi)容,詳細(xì)的內(nèi)容在以前的文章中已經(jīng)做過介紹,讀者可以返回去查看。

//定義一個(gè)接口類型
public interface Person {
    void programming();
}
//提供一個(gè)該接口的實(shí)現(xiàn)類
public class Programmer implements Person{
    private String name;

    public Programmer(String name){
        this.name = name;
    }
    @Override
    public void programming(){
        System.out.println("my name is :" + this.name);
    }
}
//自定義一個(gè)處理器
public class MyHander implements InvocationHandler {

    private Object target;

    public MyHander(Object o){
        this.target = o;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("i am programming");
        method.invoke(target,args);
        System.out.println("programming finished");
        return null;
    }
}
//生成動態(tài)代理
Programmer programmer = new Programmer("single");
MyHander hander = new MyHander(programmer);
Class proxy =       Proxy.getProxyClass(Programmer.class.getClassLoader(),new Class[]{Person.class});
Constructor constructor  = proxy.getConstructor(new Class[]{InvocationHandler.class});
Person person = (Person) constructor.newInstance(hander);
person.programming();

程序首先創(chuàng)建一個(gè)Programmer 的實(shí)例,這也是我們即將代理的實(shí)例對象。然后定義了一個(gè)處理器并將當(dāng)前代理對象實(shí)例傳入,接著通過getProxyClass傳入代理類的類加載器以及該類所有的接口即可,該方法將負(fù)責(zé)默認(rèn)實(shí)現(xiàn)所有接口中的方法并返回一個(gè)代理類型,最后我們通過反射創(chuàng)建一個(gè)代理類的實(shí)例并完成方法的調(diào)用。

程序輸出結(jié)果如下:

這里寫圖片描述

環(huán)繞著programming方法的前后,我們打印了日志信息。這是一個(gè)典型的基于jdk的動態(tài)代理的實(shí)現(xiàn)。

而我們的AOP大致也就是做這樣類似的事情,在一個(gè)方法調(diào)用之前或者之后增加一些額外的處理,具體的我們先看看幾個(gè)有關(guān)AOP的概念:

  • 切面:為目標(biāo)對象增加的每一個(gè)模塊叫做一個(gè)切面
  • 連接點(diǎn):程序執(zhí)行過程中確切的點(diǎn),方法調(diào)用前,方法調(diào)用后或者異常出現(xiàn)點(diǎn)
  • 通知:切面中的每一個(gè)方法叫做一個(gè)通知或者一次增強(qiáng)處理
  • 切入點(diǎn):可以插入增強(qiáng)處理的連接點(diǎn)叫做切入點(diǎn)

根據(jù)通知的類型不同,我們大致可以分為以下幾類:

  • before增強(qiáng)通知
  • after增強(qiáng)通知
  • around增強(qiáng)通知
  • AfterReturning增強(qiáng)通知
  • AfterThrowing增強(qiáng)通知

1、before增強(qiáng)通知處理
我們說過,Spring AOP完全依賴的AspectJ的注解,所以在使用AOP之前,我們需要引入相應(yīng)的jar包:

  • com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
  • com.springsource.org.aopalliance-1.0.0.jar
  • spring-aspects-4.0.0.RELEASE.jar
  • spring-aop-4.0.0.RELEASE.jar

before增強(qiáng)通知處理是在目標(biāo)方法執(zhí)行之前,進(jìn)行額外加強(qiáng)。例如:

//定義一個(gè)普通bean實(shí)例
@Component("runner")
public class Runner {
    private String name ="single";
    //省略setter方法
    public void run(){
        System.out.println("my name is:"+ this.name+" and i am running");
    }
}
@Aspect
@Component
public class MyAspect {

    @Before("execution(* Test_SpringAop.*.*(..))")
    public void prepareForRun(){
        System.out.println("before running, you need to do some excercises!");
    }
}

創(chuàng)建一個(gè)普通的Java類,并通過@Aspect將其申明為一個(gè)切面,@Component將其注冊到容器中。

//在容器中增加對aspectj注解的支持
<aop:aspectj-autoproxy/>

于是,我們向容器索取runner實(shí)例,并調(diào)用它的run方法,結(jié)果如下:

這里寫圖片描述

顯然,我們從容器中獲取的runner的bean實(shí)例已經(jīng)不再是普通的bean了,而是它的一個(gè)代理對象。

再回顧下整個(gè)過程,
當(dāng)容器加載的時(shí)候,掃描所有配置容器中的bean,一旦發(fā)現(xiàn)有被注解@Aspect修飾的,則注冊它為一個(gè)切面,掃描其中的方法或者叫通知,根據(jù)配置在這些方法之前的注解類型判斷當(dāng)前切面需要操作的目標(biāo)對象集。例如,我們上述使用@Before注解指定該切面對Test_SpringAop包下的所有方法進(jìn)行加強(qiáng)處理,怎么處理呢?Before指定在該調(diào)用之前,調(diào)用當(dāng)前的通知方法(prepareForRun),其實(shí)也就是生成一個(gè)動態(tài)代理的過程。于是容器中的bean都是被加強(qiáng)后代理實(shí)例。

整個(gè)過程對目標(biāo)對象中的方法未做一點(diǎn)污染,神不知,鬼不覺的增強(qiáng)了指定實(shí)例的指定方法。這就是AOP的核心思想,也是它的魅力所在。

這里寫圖片描述

@Before類型的通知類比于我們的動態(tài)代理,InvocationHandler處理器中invoke方法中,method.invoke是調(diào)用目標(biāo)的原方法,而@Before則指明在調(diào)用該目標(biāo)方法之前,插入我們指定的方法。

2、after增強(qiáng)通知處理
和before類似,after是后置處理,它指定在目標(biāo)方法調(diào)用之后插入我們指定的方法,由于與before極其類似,此處不再贅述。

3、afterReturning增強(qiáng)通知處理
afterReturning相較于after來說,它更傾向于處理有返回值的目標(biāo)方法,就是說通過使用afterReturning,我們可以獲取到剛剛執(zhí)行結(jié)束的方法的返回值。例如:

//修改runner的run方法,讓他返回name
public String run(){
    System.out.println("my name is:"+ this.name+" and i am running");
    return this.name;
}
//為切面類增加兩個(gè)通知增強(qiáng)器
@Before("execution(* Test_SpringAop.*.*(..))")
public void prepareForRun(){
    System.out.println("before running, you need to do some excercises!");
}
    
@AfterReturning(returning = "result",value = "execution(* Test_SpringAop.*.*(..))")
public void afterForRun(Object result){
    System.out.println("runner : "+ result + " is running");
}

先看結(jié)果:

這里寫圖片描述

AfterReturning指定的afterForRun(Object result)方法將在目標(biāo)方法調(diào)用之后被調(diào)用,注解中的returning 屬性的值指向目標(biāo)方法的返回值,于是我們可以在afterForRun方法中獲取原方法的返回值。

4、afterThrowing增強(qiáng)通知處理
afterThrowing主要用于處理目標(biāo)方法中未被處理的異常。例如:

//目標(biāo)方法中有一個(gè)未檢查異常
public void doMath(){
    int result = 12/0;
}
//切面中定義異常處理通知
@AfterThrowing(throwing = "ex",value = "execution(* Test_SpringAop.*.*(..))")
public void catchEx(Throwable ex){
    System.out.println("目標(biāo)方法出現(xiàn)異常:" + ex.getMessage());
}

當(dāng)我們外部調(diào)用doMath方法的時(shí)候,該未處理的異常信息將會被捕獲并輸出。

這里寫圖片描述

顯然,這種增強(qiáng)器雖然能捕獲到該異常,但是并不能對它做任何處理,程序依然拋出異常信息。它和try catch不同,被catch住的異常如果不手動拋出的話,程序是可以正常結(jié)束的。

5、around增強(qiáng)通知處理
Around類型的增強(qiáng)處理功能比較全,近乎是Before和AfterReturning兩者合起來的功能,但是它也有額外的增強(qiáng)處理,它可以控制目標(biāo)方法是否被執(zhí)行,選擇給目標(biāo)方法傳入什么形參等等。權(quán)限比較大,但是要求線程安全,所以一般建議盡量減少使用around的次數(shù)??磦€(gè)例子:

@Around("execution(* Test_SpringAop.*.*(..))")
public void useAround(ProceedingJoinPoint point) throws Throwable {
    System.out.println("i meet single");
    point.proceed(new Object[]{"single"});
    System.out.println("we leave ....");
}

被@Around修飾增強(qiáng)處理方法中,必須傳入一個(gè)ProceedingJoinPoint 類型的參數(shù),該參數(shù)代表了目標(biāo)方法,調(diào)用它的proceed方法可以控制執(zhí)行該目標(biāo)方法,并傳入?yún)?shù)。

所以說,Around其實(shí)模擬的是我們整個(gè)動態(tài)代理的過程,在這里我們可以選擇調(diào)用proceed方法與否來控制是否調(diào)用目標(biāo)方法,也可以選擇傳入?yún)?shù)與否來控制是否覆蓋原方法的參數(shù)值。

ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
Runner runner = (Runner)context.getBean("runner");
runner.sayHello("walker");

我們在main函數(shù)中調(diào)用容器為我們生成的代理對象,調(diào)用他的sayhello方法并傳入?yún)?shù)Walker,但是實(shí)際輸出結(jié)果如下:

這里寫圖片描述

實(shí)際上整個(gè)目標(biāo)方法的調(diào)用與否,參數(shù)修改與否就完全交到切面中完成了,這樣的程序靈活性很高。

以上簡單介紹了增強(qiáng)處理的幾種不同類型,除此之外,Spring還允許我們在每次增強(qiáng)方法調(diào)用時(shí)獲取到連接點(diǎn)的信息。通過向方法傳入JoinPoint類型的形參即可對目標(biāo)方法的相關(guān)信息進(jìn)行獲取。該類型中有以下幾個(gè)方法:

  • Object[] getArgs();:獲取目標(biāo)方法的所有參數(shù)
  • Signature getSignature();:獲取目標(biāo)方法的簽名
  • Object getTarget();:獲取目標(biāo)對象
@Before("execution(* Test_SpringAop.*.*(..))")
public void prepareForRun(JoinPoint point){
    System.out.println(point.getSignature());
    Object[] args = point.getArgs();
    Object obj = point.getTarget();
}

在某些情況下,這些信息還是很有作用的。

最后我們了解下,切入點(diǎn)表達(dá)式,也就是上述代碼中一直使用的:

"execution(* Test_SpringAop.*.*(..))"

這個(gè)叫做切入點(diǎn)表達(dá)式,用于定位具體的切入點(diǎn)。完整的AspectJ中具有大量的切入點(diǎn)指示符,但是Spring AOP只支持其中的部分。我們主要看其中最重要也是最常用的:execution。該指示符使用時(shí)最標(biāo)準(zhǔn)的格式為:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?.name-pattern(param-pattern) throws-pattern?)

其中:

  • modifiers-pattern:指定方法的修飾符,可以省略
  • ret-type-pattern:指定方法的返回值類型,"*"表示匹配所有的返回值類型
  • declaring-type-pattern:指定方法所屬的類,可以省略
  • name-pattern:方法名稱,"*"表示匹配所有的方法
  • param-pattern:該方法的所有形參列表,"*"表示一個(gè)任意類型的參數(shù),".."表示零個(gè)或者多個(gè)任意類型的參數(shù)。
  • throws-pattern:指定方法聲明的異常類型

接下來我們解析下我們上述一直在使用的切入點(diǎn)表達(dá)式:

"execution(* Test_SpringAop.*.*(..))"
* 匹配任意的返回值類型
Test_SpringAop.* 匹配Test_SpringAop包下的任意類
Test_SpringAop.*.*(..) 匹配了Test_SpringAop包下的任意類的任意方法,并且該方法具有零個(gè)或多個(gè)任意類型的參數(shù)

當(dāng)然,我們也可以具體到某個(gè)方法:

@Before("execution(public void Test_SpringAop.Runner.run(String,int))")

至此,有關(guān)Spring中AOP的基本知識已經(jīng)介紹完了,使用XML配置Spring AOP的方法也是類似的,此處不再贅述。總結(jié)不到之處,望指出!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評論 19 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,255評論 6 342
  • 什么是Spring Spring是一個(gè)開源的Java EE開發(fā)框架。Spring框架的核心功能可以應(yīng)用在任何Jav...
    jemmm閱讀 16,765評論 1 133
  • 01 “小丁,給我一個(gè)文案,鉆展的。”設(shè)計(jì)同事在工作QQ上說。 “哦,好的,稍等。”小丁迅速開動腦筋思索起來。 “...
    安悅微閱讀 1,391評論 9 37
  • 最近南寧失戀小哥的一段視頻火了! 翻譯:藍(lán)瘦(難受),香菇(想哭)。本來今天高高興興,你為什么要說這種話?難受,想...
    心理師靜怡閱讀 419評論 2 4

友情鏈接更多精彩內(nèi)容