Spring学习笔记

Spring学习笔记

IOC
  • 控制反转,将原本程序中手动创建UserService对象的控制权,交由Spring框架管理

  • 简单说,就是把创建UserService对象控制权被反转到了Spring框架

DI 依赖注入
  • 就是在Spring创建这个对象的过程中,将这个对象所依赖的属性注入进去

Bean标签的一些常用的属性

  • id和name

    • 一般情况下,装配一个bean的时候,通过指定一个id属性作为Bean的名称
    • id属性在ioc容器中必须的唯一的
    • 如果bean的名称中有特殊字符,就需要使用name属性
  • class

    • class用于设置一个类的全路径,用Spring来生成这个类的实例和反射
image.png

由scope配置

  • singleton是默认值,单例

    • prototype,每一次都会创建一个新的实例,也就是说是多例的

Spring的注解方式注入

image.png
  • @Component、@Repository、@Service、@Comtroller等都是相当于在配置文件中对<bean></bean>中的id的配置

    @Component("userService")//这就相当于是<bean></bean>的id;
    
    <bean id="userService" class="com.imooc.ioc.demo5.UserService">
    
  • 如果是普通类型用@Value,这个注解给key设值

    /*
        正常这时候得需要一个set方法
        然后在配置文件中使用<property name="name" value="张三"></property>进行设值
         */
        @Value("米饭")
        private String something;
    
     /*
        如果提供了set方法,这时候注解需要添加在set方法前
         */
        @Value("米饭")
        public void setSomething(String something) {
            this.something = something;
        }
    
  • 如果是对象类型的话用一下方法,例如:UserDao的注解

image.png
@Autowired
    @Qualifier("userDao")
    private UserDao userDao;

@Repository("userDao")
public class UserDao {
    public void save(){
        System.out.println("Dao层保存数据");
    }
}
//其中的@Qualifier("userDao")和@Repository("userDao")中的userDao名称一定要一致,否则会报错


//
@Resource(name = "userDao")就相当于@Autowired   @Qualifier("userDao")  //Resource也是强制用名称注入
image.png

Spring的其他注解

image.png
@Component("bean1")
public class Bean1 {

    @PostConstruct
    public void init(){
        System.out.println("initBean......");
    }

    public void say(){
        System.out.println("sayBean.......");
    }

    @PreDestroy
    public void destory(){
        System.out.println("destoryBean..........");
    }
 =======================================================================================
@Component("bean2")
@Scope("prototype")
public class Bean2 {
}
======================================================================================
    @Test
    public void demo3(){
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        Demo2 bean1 = (Demo2) applicationContext.getBean("demo2");
        Demo2 bean2 = (Demo2) applicationContext.getBean("demo2");

        System.out.println(bean1 == bean2);

    }
    ===================================================================================
  • 当bean2的Scope为默认值singleton的为单例的 ,bean1 == bean2的值为true;

  • 当bean2的Scope为prototype的为多例的 ,bean1 == bean2的值为false;

传统xml配置与注解配置混合使用

public class ProduceService {
  @Resource(name = "categoryDao")
  private CategoryDao categoryDao;

  @Resource(name = "productDao")
  private ProductDao productDao;

//    public void setCategoryDao(CategoryDao categoryDao) {
//        this.categoryDao = categoryDao;
//    }
//
//    public void setProductDao(ProductDao productDao) {
//        this.productDao = productDao;
//    }

  public  void  save(){
      System.out.println("ProduceServicede的save()方法被执行了");
      categoryDao.save();
      productDao.save();
  }
}
=========================================================================================
   <context:annotation-config/>
  <bean id="produceService" class="com.imooc.demo3.ProduceService">
<!--        <property name="categoryDao" ref="catrgoryDao"></property>-->
<!--        <property name="productDao" ref="productDao"></property>-->
  </bean>

  <bean id="productDao" class="com.imooc.demo3.ProductDao">
  </bean>

  <bean id="categoryDao" class="com.imooc.demo3.CategoryDao"></bean>
  • <<context:annotation-config/>>这个是启动属性注入

  • 正常是通过set方法

    public void setCategoryDao(CategoryDao categoryDao) {
    //        this.categoryDao = categoryDao;
    //    }
    //
    //    public void setProductDao(ProductDao productDao) {
    //        this.productDao = productDao;
    //    }
    
    • 然后使用xml配置文件配置

      <property name="categoryDao" ref="catrgoryDao"></property>
          <property name="productDao" ref="productDao"></property>
      
  • 启用<<context:annotation-config/>>后,可以通过注解方式来代替先创建set方法然后在xml配置文件中配置

    @Resource(name = "categoryDao")
        private CategoryDao categoryDao;
    
        @Resource(name = "productDao")
        private ProductDao productDao;
    

Spring AOP

public class UserDaoImpl implements UserDao{
    public void save(User user){
    public void checkPrivilege(){
        checkPrivilege()
        ...保存用户
    }
    
    public void update(User user){
        ...更新用户
    }
    
    public void delete(User user){
        ...删除用户
    }
    
    public List find( ){
        ...查询用户
    }
    
    public void checkPrivilege(){
        //检查权限
    }
}

  • 这个类中保存用户时,调用checkPrivilege()方法检查权限。但是这只是一个类,如果有很多个类,要再每一个来中都写这么一个方法然后调用时非常麻烦的
  • 传统方法:纵向继承

    public class BaseDaoImpl{
        public void checkPrivilege(){   
        }
    }
    
    public class UserDaoImpl extends BaseDaoImpl{
        public void save(User user){
            checkPrivilege()
            //保存用户
        }
    }
    
  • 没有使用AOP的方式:

  • 定义一个父类的实现类,父类中有一个检查权限的方法

  • 让其他的类继承这个父类的实现类,子类中可以有父类中的方法,直接调用checkPrivilege()检查权限

  • 使用AOP的动态代理方法:采用横向抽取机制

    • SpringAOP使用纯java实现,不需要专门的编译过程和类加载器,在运行期通过代理方式想目标类织入增强代码
  • AOP的相关术语:
image.png
image.png
  • public class UserDaoImpl implements UserDao{
        public void save(User user){
        public void checkPrivilege(){
            checkPrivilege()
            ...保存用户
        }
        
        public void update(User user){
            ...更新用户
        }
        
        public void delete(User user){
            ...删除用户
        }
        
        public List find( ){
            ...查询用户
        }
        
        public void checkPrivilege(){
            //检查权限
        }
    }
    
  • Joinpoint(连接点):指的是可以拦截到的点

    • 增删改查这些方法都可以被增强,这些方法成为是连接点
  • Pointcut(切入点):指的是真正被拦截到的点

  • 指想对save()方法进行增强(做权限校验),save方法称为是切入点

  • Advice(通知):拦截后要做的事:

    • 对save()方法进行权限校验,权限校验的方法称之为通知
      • 通知有前置和后置以及环绕通知
      • 前置通知:比如说想在save方法之前做权限校验,这就叫前置通知
      • 后置通知:比如说要在delete方法做一个日志记录,这就叫后置通知
      • 环绕通知:在之前和之后都会做一些事情
  • Target(目标):被增强的对象,例如:UserDaoImpl

  • Weaving(织入):将Advice应用到Target的过程

  • 将权限校验应用到UserDaoImpl的save()方法中的整个过程

  • Proxy(代理对象):被应用了增强后,产生的一个代理对象

    • Aspect(切面):就是切入点和通知的组合

JDK的动态代理

  • 定义一个接口
public interface UserDao {
    public void save();

    public void delete();

    public void update();

    public void find();
}
  • 接口的实现类
public class UserDaoImpl  implements UserDao{

    public void save() {
        System.out.println("保存用户");
    }

    public void delete() {
        System.out.println("删除用户");
    }

    public void update() {
        System.out.println("修改用户");
    }

    public void find() {
        System.out.println("查询用户");
    }
}
  • 编写一个动态代理类

    /*
        这个构造方法的作用是获取到需要增强的对象
         */
        public MyjdkProxy(UserDao userDao){
            this.userDao = userDao;
        }
    =====================================================================================
    public Object createProxy(){//用createProxy()这个方法产生一个代理,首选jdk的动态代理,因为jdk的动态代理可以对实现了接口的类实现代理
            /*
            传入的参数:类加载器userDao.getClass().getClassLoader();实现的接口userDao.getClass().getInterfaces();第三个参数是一个接口InvocationHandler
             */
            Object proxy = Proxy.newProxyInstance(userDao.getClass().getClassLoader(),userDao.getClass().getInterfaces(),this);
            return proxy;
        }
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if ("save".equals(method.getName())){
                System.out.println("权限校验");
                return method.invoke(userDao,args);
            }
            return method.invoke(userDao,args);
        }
    ====================================================================================
        /*
        编写测试类,使用代理
        */
        UserDao userDao = new UserDaoImpl();
    
    
            UserDao proxy = (UserDao)new MyjdkProxy(userDao).createProxy();
            proxy.save();
            proxy.delete();
            proxy.update();
            proxy.find();
        }
    

CGLIB动态代理

  • 底层:生成了一个类来继承目标类
  • 对于不使用接口的业务层,无法使用jdk的动态代理

  • CGLIB采用非常底层字节码技术,可以为一个类创建字类,解决无接口代理问题

public Object createProxy(){
    //1.创建一个CGLIB的核心类
    Enhancer enhancer = new Enhancer();
    //2.设置父类
    enhancer.setSuperclass(object.getClass());
    //3.设置回调
    enhancer.setCallback(this);
    //4.生成代理
    Object proxy = enhancer.create;
    return proxy;
}
  • 代理知识总结:

    • Spring在运行期,生成动态代理对象,不需要特殊的编译器

    • Spring AOP的底层就是通过jdk的动态代理或者CGLIB动态代理技术,为目标Bean执行横向织入

      • 若目标对象实现了若干接口,spring使用jdk的动态代理:java.lang.reflect.Proxy类代理

      • 若目标对象没事实现任何的接口,spring使用CGLIB库生成目标对象的子类

    • 程序中应优先对接口创建代理,以便于程序的解耦合以及后期的维护

    • 标记为final的方法,不能被代理,因为无法进行覆盖重写:因为她没有办法去重写父类或者父接口上面的方法

      • JDK的动态代理,是针对接口生成子类,接口中的方法不能使用final修饰
      • CGLIB是针对目标类生成子类,因此类或方法不能使用final
    • Spring只支持方法连接点,不提供属性连接点

一般切面的代码案例

  • 定义StudentDao接口
public interface StudentDao {
    public void save();

    public void find();

    public void update();

    public void delete();
}
  • 接口的实现类,实现了接口StudentDao

    public class StudentDaoImpl implements StudentDao{
        public void save() {
            System.out.println("save执行了。。。。。。。");
        }
    
        public void find() {
            System.out.println("find执行了。。。。。。。");
        }
    
        public void update() {
            System.out.println("update执行了。。。。。。");
        }
    
        public void delete() {
            System.out.println("delete执行了。。。。。。");
        }
    }
    
  • 定义一个一般切面Advisor

    public class MyBeforeAdvice implements MethodBeforeAdvice{
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println("前置通知======================");
        }
    }
    
  • 配置文件中配置

    <!-- 配置目标类 -->
        <bean id="studentDao" class="com.imooc.aop.demo3.StudentDaoImpl"></bean>
    
        <!--前置通知类型-->
        <bean id="myBeforeAdvice" class="com.imooc.aop.demo3.MyBeforeAdvice"></bean>
    
        <!--Spring AOP产生代理对象-->
        <bean id="studentDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
             <!--配置目标类-->
            <property name="target" ref="studentDao"></property>
            <!--实现的接口-->
            <property name="proxyInterfaces" value="com.imooc.aop.demo3.StudentDao"></property>
            <!--采用拦截的名称-->
            <property name="interceptorNames" value="myBeforeAdvice"></property>
        </bean>
    
image.png

Spring带有切入点的切面配置

  • 我们需要代理的对象
public class CustomerDao {
    public void find(){
        System.out.println("查询用户...");
    }

    public void save(){
        System.out.println("保存用户...");
    }

    public void update(){
        System.out.println("修改用户");
    }

    public void delete(){
        System.out.println("删除用户");
    }
  • 通知Advice
/*
环绕通知时最强大的一种通知类型,它可以阻止目标方法的执行
 */
public class MyArroundAdvice implements MethodInterceptor {

    public Object invoke(MethodInvocation invocation) throws Throwable {


        System.out.println("环绕前增强...");
        Object obj =  invocation.proceed();//用来执行目标方法,比如说原来的方法是执行保存,这句哈的意思就相当于执行保存的操作
        System.out.println("环绕后增强...");

        return obj;
    }
  • 配置
<!--配置目标类-->
    <bean id="customerDao" class="com.imooc.aop.demo4.CustomerDao"></bean>

    <!--配置通知-->
    <bean id="myArroundAdvice" class="com.imooc.aop.demo4.MyArroundAdvice"></bean>

    <!--一般的切面是使用整个通知来作为切面的,因为要对目标类某个方法或某些方法进行增强就需要配置一个带有切入点的切面-->
    <bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <!--pattern中配置正则表达式:.代表任意字符;*代表任意次数-->
        <property name="patterns" value=".*save.*,.*delete.*" ></property>
        <!--应用哪种通知-->
        <property name="advice" ref="myArroundAdvice"></property>
    </bean>

    <!--配置产生代理-->
    <bean id="customerDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="customerDao"></property>
        <property name="proxyTargetClass" value="true"></property>
        <property name="interceptorNames" value="myAdvisor"></property>
    </bean>
</beans>

自动创建代理

image.png

BeanNameAutoProxyCreate举例

  • 基于Bean名称的自动代理的创建方式,根据目标类的名称产生代理

  • 先把前几个例子中的接口,接口实现类,通知等拷贝过来

    public class CustomerDao {
        public void find(){
            System.out.println("查询用户...");
        }
    
        public void save(){
            System.out.println("保存用户...");
        }
    
        public void update(){
            System.out.println("修改用户");
        }
    
        public void delete(){
            System.out.println("删除用户");
        }
    }
    
    public interface StudentDao {
        public void save();
    
        public void find();
    
        public void update();
    
        public void delete();
    }
    
    public class StudentDaoImpl implements StudentDao {
        public void save() {
            System.out.println("save执行了。。。。。。。");
        }
    
        public void find() {
            System.out.println("find执行了。。。。。。。");
        }
    
        public void update() {
            System.out.println("update执行了。。。。。。");
        }
    
        public void delete() {
            System.out.println("delete执行了。。。。。。");
        }
    }
    
public class MyBeforeAdvice implements MethodBeforeAdvice {

    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("前置通知======================");
    }
}
public class MyArroundAdvice implements MethodInterceptor {

    public Object invoke(MethodInvocation invocation) throws Throwable {


        System.out.println("环绕前增强...");
        Object obj =  invocation.proceed();//用来执行目标方法,比如说原来的方法是执行保存,这句哈的意思就相当于执行保存的操作
        System.out.println("环绕后增强...");

        return obj;
    }
}
  • 配置文件配置

        <!--配置目标类-->
        <bean id="studentDao" class="com.imooc.aop.demo5.StudentDaoImpl"></bean>
        <bean id="customerDao" class="com.imooc.aop.demo5.CustomerDao"></bean>
    
        <!--配置增强,Advice-->
        <bean id="myArroundAdvice" class="com.imooc.aop.demo5.MyArroundAdvice"></bean>
        <bean id="myBedoreAdvice" class="com.imooc.aop.demo5.MyBeforeAdvice"></bean>
    
        <!--自动代理-->
        <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
            <property name="beanNames" value="*Dao"></property>
            <property name="interceptorNames" value="myBedoreAdvice"></property>
        </bean>
    
    </beans>
    
    • 这种自动代理有缺陷,比如说只想增强其中的save方法该怎么做到呢,这是基于Bean名称的方式又不太合适了

DefalutAdvisorAutoProxyCreator举例

  • 默认的切面的自动代理的方式创建,这种方式可以基于切面的信息自动产生代理

  • 前几个步骤与上一个方法类似,只是在配置文件中不同

    <!--配置目标类-->
        <bean id="studentDao" class="com.imooc.aop.demo6.StudentDaoImpl"></bean>
        <bean id="customerDao" class="com.imooc.aop.demo6.CustomerDao"></bean>
    
        <!--配置增强,Advice-->
        <bean id="myArroundAdvice" class="com.imooc.aop.demo6.MyArroundAdvice"></bean>
        <bean id="myBedoreAdvice" class="com.imooc.aop.demo6.MyBeforeAdvice"></bean>
    
        <!--配置一个切面-->
        <bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
            <property name="pattern" value="com\.imooc\.aop\.demo6\.CustomerDao\.save"></property>
            <property name="advice" ref="myArroundAdvice"></property>
        </bean>
    
        <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
        </bean>
    
    • 配置了一个切面,因为是基于切面的信息自动产生代理
    • 在切面配置中,<property name="pattern" value="com.imooc.aop.demo6.CustomerDao.save"></property>;这一步就是根据类中的方法来增强,由于是正则表达式,所有需要对.进行转义\

AspectJ的注解AOP开发

  • AspectJ简介
    • AspectJ是一个基于Java语言的AOP框架
    • Spring2.0以后新增了对AspectJ切点表达式的支持
    • @AspectJ是AspectJ1.5新增的功能,通过JDK5注解技术,允许直接在Bean类中定义切面
    • 新版本的Spring框架,都建议使用AspectJ方式来开发AOP


      image.png

为目标类,定义切面类

public class PeoductDao {
    public void save(){
        System.out.println("保存商品");
    }

    public void delete(){
        System.out.println("删除商品");
    }

    public void update(){
        System.out.println("修改商品");
    }

    public void findOne(){
        System.out.println("查询一个商品");

    }

    public void findAll(){
        System.out.println("查询所有的商品");
    }
/*
切面类
 */
@Aspect
public class MyAspect {
    @Before(value = "execution(* com.imooc.aspectJ.demo1.PeoductDao.save(..))")
    public void before(){
        System.out.println("前置通知");
    }
<!--开启AspectJ的注解开发,自动代理-->
    <aop:aspectj-autoproxy/>

    <bean id="peoductDao" class="com.imooc.aspectJ.demo1.PeoductDao"></bean>

    <!--定义切面-->
    <bean class="com.imooc.aspectJ.demo1.MyAspect"></bean>

详细介绍Before前置通知

image.png

@AfterReturing 后置通知

    @AfterReturning(value = "execution(* com.imooc.aspectJ.demo1.PeoductDao.update(..))",returning = "re")
    public void afterReturing(Object re){//为什么要用Object,因为这个返回值有可能是任意类型的返回值,所以用Object;
        System.out.println("后置通知=============" + re);
    }
  • //为什么要用Object,因为这个返回值有可能是任意类型的返回值,所以用Object;

  • @AfterReturing 异常抛出通知

@Around环绕通知

image.png
  • 重点:如果不调用joinPoint.proceed();方法的话,目标方法会被拦截,即delete不会执行

通过@Pointcut为切点命名

  • 在每个通知内定义切点,会造成工作量大,不易维护,对于重复的切点,可以使用@Pointcut进行定义

  • 切点方法:private void 无参数方法。方法名为切点名;因为这个方法在类中没有什么实际的意义,也不用在别的方法中调用它,所以定义为private

  • 当通知多个切点时,可以使用||进行连接

基于AspectJ的xml方式的AOP开发

  • 通过配置的方式完成AOP的开发

  • public interface CustomerDao {
        public void save();
    
        public void findOne();
    
        public void fingAll();
    
        public void update();
    
        public void delete();
    }
    
  • public class CustomerDaoImpl implements CustomerDao {
        public void save() {
          System.out.println("保存客户");
        }
    
        public void findOne() {
            System.out.println("查找一个客户");
    
        }
    
        public void fingAll() {
            System.out.println("查找所有的客户");
        }
    
        public void update() {
            System.out.println("修改客户");
    
        }
    
        public void delete() {
            System.out.println("删除客户");
    
        }
    
  • <!--xml配置方式完成AOP的开发-->
     
     <!--配置目标类-->
       <bean id="customerDao" class="com.imooc.aspectJ.demo2.CustomerDaoImpl"></bean>
    
image.png
image.png

image.png

JDBC Template

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容