Java框架-02-Spring

1. Spring简介

Spring的特点:

  1. 方便解耦,简化开发
  2. AOP编程的支持
  3. 声明式事务的支持
  4. 方便程序的测试
  5. 方便集成各种优秀框架
  6. 降低Java EE API的使用难度

Spring的优点:Spring的目标是使已存在的技术更加易用

  1. 低侵入式设计,代码污染极低
  2. 独立于各种应用服务器
  3. Spring的DI机制降低了业务对象替换的复杂性,提高了组件之间的解耦
  4. Spring的AOP支持允许将一些通用任务如安全、事务、日志等进行集中式管理,从而提供了更好的复用
  5. Spring的ORM和DAO提供了与第三方持久层框架的良好整合,并简化了底层的数据库访问
  6. Spring并不强制应用完全依赖于Spring,开发者可自由选用Spring框架的部分或全部

Spring体系结构:

名词解释:

  1. IoC(Inversion of Control):控制反转,是面向对象编程中的依赖倒转原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做DI(Dependency Injection,依赖注入)。
  2. AOP(Aspect Oriented Programming):面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。
  3. ORM(Object Relational Mapping):对象关系映射,用于实现面向对象编程语言里不同类型系统的数据之间的转换。
  4. OXM(Object XML Mapping):指数据实体对象与XML节点之间的映射。
  5. SpEL(Spring Expression Language):Spring表达式语言

2. Spring中的控制反转-IoC*

自定义IoC的思路(使用简单工厂模式解耦):加载工厂类时解析xml文件,获取所有bean标签的id属性和class属性,根据class属性创建类的对象,并放入到容器Map(id,object)中,对外提供一个根据id在容器里取对象的静态方法。

Spring中的控制反转:由Spring框架创建和管理对象,需要使用时,直接从Spring的工厂(容器)里取。

Spring中的IoC使用:

  1. 创建Maven工程,添加相关坐标*

    <dependencies>
        <!--spring的核心容器context-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <!--spring的jdbc-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <!--aop-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.7</version>
        </dependency>
    
        <!--junit相关 start-->
        <!--spring整合junit-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>
        <!--junit-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!--junit相关 end-->
    
        <!--mybatis相关 start-->
        <!--spring整合mybatis-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.3.0</version>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.3</version>
        </dependency>
        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <!--数据库连接池-->
        <!--mybatis相关 end-->
    
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
            <scope>provided</scope>
        </dependency>
        <!--日志相关 start-->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.12</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.6.6</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.6.6</version>
        </dependency>
        <!--日志相关 end-->
    </dependencies>
    
  2. 准备接口和实现类

    // 接口
    public interface PojoDao {
        void update();
    }
    
    // 实现类
    public class PojoDaoImpl implements PojoDao {
        public void init() {
            System.out.println("PojoDaoImpl.init()");
        }
    
        @Override
        public void update() {
            System.out.println("PojoDaoImpl.update()");
        }
    
        public void destroy() {
            System.out.println("PojoDaoImpl.destroy()");
        }
    }
    
  3. 在resources下创建Spring的XML配置文件(任意名称,如:applicationContext.xml),配置bean标签*

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">
        <!--
        一个实现类对应一个bean标签:
        class:实现类的全限定名
        id:可以根据id从核心容器中获取对象
        scope:对象的作用域(对象在IoC容器中的生命周期)
            singleton单例(默认)
            prototype原型
            request针对Web应用,创建对象时会将此对象存储到request作用域
            session针对Web应用,创建对象时会将此对象存储到session作用域
            单例模式下(默认没有开启懒加载):
                核心容器创建时,创建出该类对象
                核心容器销毁时,该类对象被销毁
            原型模式下:
                核心容器调用getBean(id)时,创建出该类对象
                垃圾回收机制销毁该对象
        lazy-init:配置懒加载,核心容器创建时是否创建出该类的对象
        init-method:配置该类对象初始化时调用的方法
        destroy-method:配置该类对象销毁时调用的方法,针对的是单例
        -->
        <bean class="com.liu2m.dao.impl.PojoDaoImpl"
              id="pojoDao"
              scope="singleton"
              lazy-init="false"
              init-method="init"
              destroy-method="destroy">
        </bean>
    </beans>
    
  4. 创建Spring的核心容器来获取bean*

    import com.liu2m.dao.PojoDao;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class SpringTest {
        @Test
        public void test01() {
            // 1. 创建Spring的核心容器ApplicationContext
            // 创建核心容器时,就读取了整个配置文件,并将bean标签对应的对象创建出来放到核心容器中了
            // ClassPathXmlApplicationContext:从类的根路径下加载xml配置文件
            // FileSystemXmlApplicationContext:从磁盘路径上加载xml配置文件
            // AnnotationConfigApplicationContext:用注解配置容器对象时使用
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
            // XmlBeanFactory是老版本使用的工厂,目前已被废弃
            // BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("applicationContext.xml"));
            // 2. 调用核心容器的方法,根据id获取对象
            PojoDao pojoDao = (PojoDao) applicationContext.getBean("pojoDao");
            // 使用对象
            pojoDao.update(); // PojoDaoImpl.update()
        }
    }
    

ApplicationContext和BeanFactory的区别:

  1. ApplicationContext是BeanFactory的子接口
  2. BeanFactory的加载方式只有懒加载
  3. ApplicationContext的加载方式有懒加载和非懒加载
  • 各种ApplicationContext和BeanFactory的实现都用到了模板方法模式

实例化Bean的方式:

  1. 使用无参构造来实例化Bean*

    <bean class="com.liu2m.dao.impl.PojoDaoImpl"
          id="pojoDao">
    </bean>
    
  2. 使用工厂类来实例化Bean

    // 创建工厂类
    public class PojoDaoFactory {
        // 方法需要是静态的
        public static PojoDao createPojoDaoImpl() {
            return new PojoDaoImpl();
        }
    }
    
    <!--class:工厂类的全限定名
    factory-method:调用工厂的哪个方法-->
    <bean class="com.liu2m.dao.PojoDaoFactory"
          factory-method="createPojoDaoImpl"
          id="pojoDao">
    </bean>
    
  3. 使用工厂对象来实例化Bean

    // 创建工厂类
    public class PojoDaoFactory {
        // 方法需要是非静态的
        public PojoDao createPojoDaoImpl() {
            return new PojoDaoImpl();
        }
    }
    
    <!--配置工厂类-->
    <bean class="com.liu2m.dao.PojoDaoFactory"
          id="pojoDaoFactory">
    </bean>
    <!--factory-bean:工厂对象的id
    factory-method:调用工厂的哪个方法-->
    <bean factory-bean="pojoDaoFactory"
          factory-method="createPojoDaoImpl"
          id="pojoDao">
    </bean>
    

3. Spring中的依赖注入-DI*

Spring中的依赖注入:Spring在创建类的对象时,顺便给对象的属性赋值(注入)

环境准备:

public interface PojoService {
    void update();
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class PojoServiceImpl implements PojoService {
    private PojoDao pojoDao;
    private String simple;
    private String[] array;
    private Map map;

    @Override
    public void update() {
        pojoDao.update();
        System.out.println(this);
    }
}

实现依赖注入的方式:

  1. 使用构造方法注入+:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">
        <!--PojoDaoImpl-->
        <bean class="com.liu2m.dao.impl.PojoDaoImpl"
              id="pojoDao">
        </bean>
        <!--PojoServiceImpl-->
        <!--构造方法方式注入需要提供构造方法
            1. 建议构造注入时使用index不用name,
            因为Java只会维护形参的数据类型,不会维护形参的名称,通过name进行注入时可能会出现风险
            2. 如果类中的构造方法重载较多,出现参数个数相同的情况,需要指定参数类型-->
        <bean class="com.liu2m.service.impl.PojoServiceImpl"
              id="pojoService">
            <!--使用有参构造给对象类型属性pojoDao注入
            name:构造参数的名称
            index:构造参数的位置
            ref:对象的id
            type:指定参数类型-->
            <constructor-arg name="pojoDao" ref="pojoDao"></constructor-arg>
            <!--使用有参构造给简单类型属性simple注入-->
            <constructor-arg name="simple" value="值"></constructor-arg>
            <!--使用有参构造给数组类型属性array注入-->
            <constructor-arg name="array">
                <array>
                    <value>0</value>
                    <value>1</value>
                </array>
            </constructor-arg>
            <!--使用有参构造给Map类型属性map注入-->
            <constructor-arg name="map">
                <map>
                    <entry key="键0" value="值0"></entry>
                    <entry key="键1" value="值1"></entry>
                </map>
            </constructor-arg>
        </bean>
    </beans>
    
  2. 使用set方法注入*:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">
        <!--PojoDaoImpl-->
        <bean class="com.liu2m.dao.impl.PojoDaoImpl"
              id="pojoDao">
        </bean>
        <!--PojoServiceImpl-->
        <!--set方法方式注入需要提供set方法-->
        <bean class="com.liu2m.service.impl.PojoServiceImpl"
              id="pojoService">
            <!--使用set方法给对象类型属性pojoDao注入
            name:属性名
            ref:对象的id-->
            <property name="pojoDao" ref="pojoDao"></property>
            <!--使用set方法给简单类型属性simple注入-->
            <property name="simple" value="值"></property>
            <!--使用set方法给数组类型属性array注入-->
            <property name="array">
                <array>
                    <value>0</value>
                    <value>1</value>
                </array>
            </property>
            <!--使用set方法给Map类型属性map注入-->
            <property name="map">
                <map>
                    <entry key="键0" value="值0"></entry>
                    <entry key="键1" value="值1"></entry>
                </map>
            </property>
        </bean>
    </beans>
    
  3. 使用p命名空间注入-:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:p="http://www.springframework.org/schema/p"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">
        <!--PojoDaoImpl-->
        <bean class="com.liu2m.dao.impl.PojoDaoImpl"
              id="pojoDao">
        </bean>
        <!--PojoServiceImpl-->
        <!--P名称空间注入:底层也是调用set方法,只是写法不同
        需要引入:xmlns:p="http://www.springframework.org/schema/p"-->
        <bean class="com.liu2m.service.impl.PojoServiceImpl"
              id="pojoService"
              p:pojoDao-ref="pojoDao"
              p:simple="值">
        </bean>
    </beans>
    

4. Spring中的面向切面编程-AOP*

OOP针对业务处理过程中的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。

AOP针对业务处理过程中的某个步骤或阶段进行抽象封装,以获得逻辑过程中各部分之间低耦合性的隔离效果。

AOP的主要意图:将业务逻辑代码中重复的行为抽取出来,进而改变这些行为的时候不影响业务逻辑的代码。

Spring中AOP的实现原理就是动态代理,Spring会根据目标类是否实现了接口来决定采用哪种动态代理的方式。

AOP中的术语:

  1. JoinPoint:连接点(所有可以被增强的方法),在Spring的AOP中,指业务层类的所有现有的方法
  2. Pointcut:切入点(真正需要增强的方法)
  3. Advice:通知/增强(具体用于增强的代码)
    1. 前置通知:在目标方法执行前执行
    2. 后置通知:在目标方法执行完也没有抛出异常时执行,可以得到被增强方法的返回值
    3. 异常通知:在目标方法抛出异常时执行,可以获得异常的信息
    4. 最终通知:在目标方法执行之后无论有没有异常都执行
    5. 环绕通知:在目标方法之前和之后执行,可以阻止目标方法执行
  4. Aspect:切面(切入点与通知间的关系,让通知和切入点进行关联)

开发阶段可以先编写核心业务代码;然后把公用代码抽取出来制作成切面中的通知;最后在配置文件中声明切面。Spring框架在运行阶段会监控切入点方法的执行,一旦监控到切入点方法被执行,就使用动态代理创建目标对象的代理对象,根据通知类别,在代理对象的对应位置将通知对应的功能织入,完成完整的代码逻辑运行。

常用切入点表达式:

任意public方法:execution(public * *(..))
任意以set开头的方法:execution(* set*(..))
某个类的任意方法:execution(* 类的全限定名.*(..))
某个包下任意类的任意方法(不含子包):
    1. execution(* 包的全限定名.*.*(..))
    2. within(包的全限定名.*)
某个包下任意类的任意方法(包含子包):
    1. execution(* 包的全限定名..*.*(..))
    2. within(包的全限定名..*)
实现当前接口的类的任意方法:
    1. this(接口的全限定名)
    2. target(接口的全限定名)
只有一个参数且实现了Serializable的任意方法:args(java.io.Serializable)
只有一个参数且参数有Classified注解的任意方法:@args(Classified注解的全限定名)
有Transactional注解的方法:
    1. @target(org.springframework.transaction.annotation.Transactional)
    2. @within(org.springframework.transaction.annotation.Transactional)
    3. @annotation(org.springframework.transaction.annotation.Transactional)
指定名称bean下的任意方法:
    1. bean(名称)
    2. bean(*名称)

使用配置文件配置Spring的AOP:

  1. 定义需要被增强的业务逻辑类:

    /**
     * 目标:
     * 1. 在执行CRUD方法之前,使用前置通知before
     * 2. 在执行CRUD方法之后,若没有抛出异常则使用后置通知after-returning
     * 3. 在执行CRUD方法抛出异常之后,使用异常通知after-throwing
     * 4. 在执行CRUD方法之后无论有没有抛出异常,均使用最终通知after
     * 5. 使用环绕通知around,计算Update方法的执行时间
     */
    public class PojoServiceImpl implements PojoService {
        @Override
        public void create() {
            System.out.println("PojoServiceImpl.create()");
        }
    
        @Override
        public void retrieve() {
            System.out.println("PojoServiceImpl.retrieve()");
        }
    
        @Override
        public void update() {
            try {
                Thread.sleep(3000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("PojoServiceImpl.update()");
        }
    
        @Override
        public void delete() {
            System.out.println("PojoServiceImpl.delete()");
            int exception = 1 / 0;
        }
    }
    
  2. 定义切面类:

    public class PermissionAspect {
        public void useBefore() {
            System.out.println("PermissionAspect.useBefore()");
        }
    
        public void useAfterReturning() {
            System.out.println("PermissionAspect.useAfterReturning()");
        }
    
        public void useAfterThrowing() {
            System.out.println("PermissionAspect.useAfterThrowing()");
        }
    
        public void useAfter() {
            System.out.println("PermissionAspect.useAfter()");
        }
    
        public void useAround(ProceedingJoinPoint joinPoint) {
            long start = System.currentTimeMillis();
            try {
                // 执行目标方法
                joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
            long end = System.currentTimeMillis();
            System.out.println("useAround():end - start = " + (end - start));
        }
    }
    
  3. 在Spring的配置文件中配置AOP:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd">
        <!--1. 对需要被增强的业务逻辑类进行IoC-->
        <bean class="com.liu2m.service.impl.PojoServiceImpl" id="pojoService"></bean>
        <!--2. 对切面类进行IoC-->
        <bean class="com.liu2m.aspect.PermissionAspect" id="permissionAspect"></bean>
        <!--3. 配置AOP-->
        <aop:config>
            <!--3.1 配置切入点
            id:切入点的id
            expression:切入点表达式-->
            <aop:pointcut id="point1" expression="execution(* com.liu2m.service.impl.PojoServiceImpl.*(..))"/>
            <aop:pointcut id="point2" expression="execution(* com.liu2m.service.impl.PojoServiceImpl.update(..))"/>
            <!--3.2 配置切面
            ref:切面对象的id
            id:切面的id-->
            <aop:aspect ref="permissionAspect" id="aspect1">
                <!--3.3 配置通知,并关联通知和切入点:将method的作用到pointcut-ref上
                method:将这个切面中的哪个方法作为该通知
                pointcut-ref:切入点的id,或使用pointcut指定切入点表达式-->
                <!--前置通知-->
                <aop:before method="useBefore" pointcut-ref="point1"></aop:before>
                <!--后置通知-->
                <aop:after-returning method="useAfterReturning" pointcut-ref="point1"></aop:after-returning>
                <!--异常通知-->
                <aop:after-throwing method="useAfterThrowing" pointcut-ref="point1"></aop:after-throwing>
                <!--最终通知-->
                <aop:after method="useAfter" pointcut-ref="point1"></aop:after>
                <!--环绕通知-->
                <aop:around method="useAround" pointcut-ref="point2"></aop:around>
            </aop:aspect>
        </aop:config>
    </beans>
    
  4. 单元测试:

    import com.liu2m.service.PojoService;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = "classpath:applicationContext.xml")
    public class SpringTest {
        // 自动注入要使用的对象
        @Autowired
        private PojoService pojoService;
    
        @Test
        public void test01() {
            System.out.println("---------pojoService.create():一起正常---------");
            pojoService.create();
            System.out.println("---------pojoService.retrieve():一切正常---------");
            pojoService.retrieve();
            System.out.println("---------pojoService.update():计算执行时间---------");
            pojoService.update();
            System.out.println("---------pojoService.delete():抛出异常---------");
            pojoService.delete();
        }
    }
    

5. Spring的JdbcTemplate-

JdbcTemplate对原生的JDBC进行了简单的封装

使用步骤:

  1. 导入坐标

  2. 传递DataSource创建JdbcTemplate对象

    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    
  3. 定义SQL语句:SQL的参数使用?作为占位符

  4. 调用JdbcTemplate的方法来执行SQL

    1. update():执行DML增删改语句

    2. queryForMap():将列名作为key,将值作为value,将这条记录封装为一个map集合。这个方法查询的结果集长度只能是1

    3. queryForList():将每一条记录封装为一个Map集合,再将Map集合装载到List集合中

    4. query():查询结果,将结果封装为JavaBean对象;query的参数是RowMapper,一般可以使用其实现类BeanPropertyRowMapper<类型>(类型.class)完成数据到JavaBean的自动封装。其中的思想是策略模式,RowMapper是抽象策略,其每一个实现类都是一个具体策略。

    5. queryForObject():查询结果,将结果封装为对象。一般用于聚合函数的查询

JdbcTemplate使用示例:

import org.junit.Test;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

public class JdbcTemplateTest {
    // 1. 传递DataSource创建JdbcTemplate对象
    // 使用自定义的DruidUtils获取数据库连接池对象
    private static JdbcTemplate jdbcTemplate = new JdbcTemplate(DruidUtils.getDataSource());

    // 查询所有记录,将其封装为User对象的List集合
    @Test
    public void test01() {
        // 2. 定义SQL语句:SQL的参数使用?作为占位符
        String sql = "select * from user";
        // 3. 调用JdbcTemplate的方法来执行SQL
        List<User> userList = jdbcTemplate.query(sql, new RowMapper<User>() {
            @Override
            public User mapRow(ResultSet resultSet, int i) throws SQLException {
                User user = new User();
                int id = resultSet.getInt("id");
                String username = resultSet.getString("username");
                String password = resultSet.getString("password");
                double balance = resultSet.getDouble("balance");
                user.setId(id);
                user.setUsername(username);
                user.setPassword(password);
                user.setBalance(balance);
                return user;
            }
        });
        for (User user : userList) {
            System.out.println(user);
        }
    }

    // 查询所有记录,将其封装为User对象的List集合
    @Test
    public void test02() {
        // 2. 定义SQL语句:SQL的参数使用?作为占位符
        String sql = "select * from user";
        // 3. 调用JdbcTemplate的方法来执行SQL
        List<User> userList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<User>(User.class));
        for (User user : userList) {
            System.out.println(user);
        }
    }
}

6. Spring管理事务

PlatformTransactionManager:Spring为事务定义了一个统一的接口,其实现类就是Spring真正管理事务的对象

  1. 如果Dao层使用的是JDBC、JdbcTemplate、MyBatis,则可以使用DataSourceTransactionManager来处理事务
  2. 如果Dao层使用的是Hibernate,则可以使用HibernateTransactionManager来处理事务

TransactionDefinition:定义了事务的隔离级别、传播行为、超时信息等

TransactionStatus:定义事务是否是新事务、是否有保存点等

6.1 编程式事务-

编程式事务使用:

  1. 创建事务管理者,并传入数据源
  2. 创建事务模板,并传入事务管理者
  3. 通过事务模版处理事务
@Service("userService")
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;
    @Autowired
    private DataSource dataSource;

    @Override
    public void transfer(String fromName, String toName, double money) {
        // 1. 创建事务管理者,并传入数据源
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
        // 2. 创建事务模板,并传入事务管理者
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        // 3. 通过事务模版处理事务
        transactionTemplate.execute(new TransactionCallback<Object>() {
            @Override
            public Object doInTransaction(TransactionStatus transactionStatus) {
                // 事务第1步:转出账号扣款
                userDao.updateMoney(new User(fromName, -money));
                // 模拟异常
                int exception = 1 / 0;
                // 事务第2步:转入账号收款
                userDao.updateMoney(new User(toName, +money));
                return null;
            }
        });
    }
}

6.2 声明式事务*

Spring声明式事务的作用:无论Spring集成什么Dao框架,事务代码都不需要再编写了。声明式事务的思想就是使用AOP,以面向切面的方式完成事务的管理。

事务的传播特性解决的问题:如果Service层的某个方法中除了调用Dao层的方法,还调用了本类其他的Service方法,那么在调用其他Service方法时,必须保证两个Service处在同一个事务中,确保事物的一致性。

使用xml配置文件配置声明式事务:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!--使用xml配置文件配置声明式事务-->
    <!--导入dataSource所在的配置文件,
    之后在Spring配置文件中只需要导入当前配置文件,
    不需要再引入dataSource所在的配置文件-->
    <import resource="classpath:applicationMyBatis.xml"></import>
    <!--1. 配置事务管理者-->
    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--2. 配置事务的规则-->
    <tx:advice id="transferAdvice" transaction-manager="transactionManager">
        <!--配置事务的属性-->
        <tx:attributes>
            <!--
            name:方法名
            rollback-for="java.lang.Exception":遇到Exception时回滚
            no-rollback-for="java.lang.NullPointerException":遇到NullPointerException时不回滚
            timeout:事务的超时时间,默认不超时
            read-only:是否是只读事务;只读事务针对的是查询,增删改不能设置为只读
            isolation:事务的隔离级别,值:DEFAULT、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE
            propagation:事务的传播行为,值:
                保证在同一个事务里执行:
                REQUIRED:默认值;如果当前没有事务,就新建一个事务;如果已经存在一个事务,加入到这个事务中
                SUPPORTS:如果当前没有事务,就以非事务方式执行;如果已经存在一个事务,加入到这个事务中
                MANDATORY:如果当前没有事务,就抛出异常;如果已经存在一个事务,加入到这个事务中
                保证在不同事务里执行:
                REQUIRES_NEW:新建事务,独立执行;如果当前存在事务,把当前事务挂起
                NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,就把当前事务挂起
                NEVER:以非事务方式执行,如果当前存在事务,则抛出异常
                NESTED:如果当前存在事务,则执行一个嵌套事务;如果当前没有事务,就新建一个事务
            -->
            <tx:method name="transfer"
                       rollback-for="java.lang.Exception"
                       no-rollback-for="java.lang.NullPointerException"/>
        </tx:attributes>
    </tx:advice>
    <!--3. 配置事务的AOP-->
    <aop:config>
        <!--声明切入点:需要事务的方法-->
        <aop:pointcut id="pointcut1" expression="execution(* com.liu2m.service.impl.UserServiceImpl.transfer(..))"/>
        <!--绑定切入点和事务
        advice-ref:advice的id-->
        <aop:advisor advice-ref="transferAdvice" pointcut-ref="pointcut1"></aop:advisor>
    </aop:config>
</beans>

7. Spring常用注解*

注解使用前要在配置文件中配置包扫描

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!--base-package指定要扫描的包-->
    <context:component-scan base-package="com.liu2m"></context:component-scan>
</beans>

配置文件方便维护,注解方便使用

建议:自己写的类用注解进行IoC,不是自己写的类用配置文件进行IoC;类用什么进行的IoC,属性就用什么进行DI

7.1 IoC相关注解

以下四个注解都可以对类进行IoC,但相应的类上最好用相应的注解:

  1. @Controller:一般用在Web层的各种Controller类上
  2. @Service:一般用在Service层的各种Service类上
  3. @Repository:一般用在DAO层的各种Dao类上(MyBatis没有Dao实现类,所以这个注解基本不用)
  4. @Component:一般用在除了三层结构的其他类上
  • 以上注解的value属性就是id,如果不指定value属性,则id为类名首字母小写

scope的注解:@Scope("prototype"),默认值为singleton

lazy-init的注解:@Lazy(false),默认值为true

init-method的注解:@PostConstruct,作用在方法上

destroy-method的注解:@PreDestroy,作用在方法上

7.2 DI相关注解

注解注入属性使用的是暴力反射,set方法可以省略

注入简单类型:@Value("值")

注入对象类型:建议只有一个实现类时使用@Autowired,有多个实现类时使用@Resource

  1. @Autowired:按照类型自动注入其他bean对象
    1. 如果Spring核心容器里只有一个该类型对象,则可以自动注入
    2. 如果Spring核心容器里有多个该类型对象,则找id和属性名一致的对象注入,找不到就抛异常
  2. @Qualifier("bean的id"):在@Autowired的基础之上,再按照bean的id注入;必须和@Autowired一起使用,不能独立使用,但给方法参数注入时,可以独立使用
  3. @Resource(name = "bean的id"):能够进行自动装配以及手动装配;如果一个接口有多个实现类,可以使用@Resource指定找具体的某一个实现

7.3 AOP相关注解

@Aspect:指定该类是切面类

@Pointcut("切入点表达式"):作用在方法上

@Before("切入点"):前置通知

@AfterReturning("切入点"):后置通知

@AfterThrowing("切入点"):异常通知

@After("切入点"):最终通知

@Around("切入点"):环绕通知

使用注解配置Spring的AOP:

  1. 定义需要被增强的业务逻辑类,并使用注解对其进行IoC

  2. 定义切面类:

    @Component
    @Aspect
    public class PermissionAspect {
        @Pointcut("execution(* com.liu2m.service.impl.PojoServiceImpl.*(..))")
        public void point1() {
        }
    
        @Pointcut("execution(* com.liu2m.service.impl.PojoServiceImpl.update(..))")
        public void point2() {
        }
    
        @Before("point1()")
        public void useBefore() {
            System.out.println("PermissionAspect.useBefore()");
        }
    
        @AfterReturning("point1()")
        public void useAfterReturning() {
            System.out.println("PermissionAspect.useAfterReturning()");
        }
    
        @AfterThrowing("point1()")
        public void useAfterThrowing() {
            System.out.println("PermissionAspect.useAfterThrowing()");
        }
    
        @After("point1()")
        public void useAfter() {
            System.out.println("PermissionAspect.useAfter()");
        }
    
        @Around("point2()")
        public void useAround(ProceedingJoinPoint joinPoint) {
            long start = System.currentTimeMillis();
            try {
                // 执行目标方法
                joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
            long end = System.currentTimeMillis();
            System.out.println("useAround():end - start = " + (end - start));
        }
    }
    
  3. 在Spring的配置文件中加载AOP的注解驱动:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
        <!--包扫描:base-package指定要扫描注解的包-->
        <context:component-scan base-package="com.liu2m"></context:component-scan>
        <!--加载AOP的注解驱动-->
        <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    </beans>
    

7.4 声明式事务相关注解

@Transactional(属性 = 值, 属性 = 值):

  1. 作用在方法上:指定该方法需要使用事务
  2. 作用在类上:指定该类所有方法都需要使用事务

使用注解配置声明式事务:

  1. 在Spring的xml配置文件中配置事务管理者并加载事务注解驱动
  2. 在要使用事务的方法上添加@Transactional注解
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!--使用注解配置声明式事务-->
    <!--导入dataSource所在的配置文件,
    之后在Spring配置文件中只需要导入当前配置文件,
    不需要再引入dataSource所在的配置文件-->
    <import resource="classpath:applicationMyBatis.xml"></import>
    <!--1. 配置事务管理者-->
    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!--2. 加载事务注解驱动
    transaction-manager:事务管理者的id-->
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
    <!--3. 在要使用事务的方法上添加@Transactional注解-->
</beans>

7.5 配置相关注解

component-scan的注解:@ComponentScan(basePackages = "com.liu2m")

@Configuration:指定当前类是一个Spring配置类,获取核心容器时需要的是AnnotationConfigApplicationContext(类.class)

@Import({其他配置类的.class字节码对象}):引入其他配置类,在引入其他配置类时,可以不用再写@Configuration注解

@PropertySource({"classpath:properties文件的位置"}):加载.properties文件中的配置

@Bean(name = "当前方法创建出的对象的id"):只能写在方法上,表示将此方法返回的对象放入到Spring容器中

8. Spring整合其他框架

8.1 Spring整合数据库连接池

整合后Dao层使用时,可直接注入dataSource

Spring整合C3P0连接池:

  1. 导入连接池坐标

    <dependency>
        <groupId>c3p0</groupId>
        <artifactId>c3p0</artifactId>
        <version>0.9.1.2</version>
    </dependency>
    
  2. 配置数据源对象dataSource

    <!--配置C3P0的数据源对象dataSource-->
    <bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/db01?useSSL=false"></property>
        <property name="user" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>
    

Spring整合Druid连接池:

  1. 导入连接池坐标

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.18</version>
    </dependency>
    
  2. 配置数据源对象dataSource

    <!--配置Druid的数据源对象dataSource-->
    <bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://localhost:3306/db01?useSSL=false"></property>
        <property name="username" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>
    

Spring整合HikariCP连接池:

  1. 导入连接池坐标

    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>3.1.0</version>
    </dependency>
    
  2. 配置数据源对象dataSource

    <!--配置HikariCP的数据源对象dataSource-->
    <bean class="com.zaxxer.hikari.HikariDataSource" id="dataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/db01?useSSL=false"></property>
        <property name="username" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>
    

8.2 Spring整合MyBatis*

整合后可以不写MyBatis的核心配置文件,使用时直接根据id从Spring核心容器中获取PojoDao的代理对象

导入坐标后,使用Spring配置文件来整合*:

  1. 在resources下创建jdbc.properties:

    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/db01?useSSL=false
    jdbc.username=root
    jdbc.password=123456
    
  2. 在resources下创建整合mybatis的配置文件applicationMyBatis.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
        <!--spring整合mybatis start-->
        <!--引入jdbc的properties配置文件,properties配置文件中的key一定要是带点分隔的-->
        <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
        <!--1. 配置dataSource
        可以使用Spring内置的DriverManagerDataSource,也可以使用第三方的DataSource-->
        <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
            <property name="driverClassName" value="${jdbc.driver}"></property>
            <property name="url" value="${jdbc.url}"></property>
            <property name="username" value="${jdbc.username}"></property>
            <property name="password" value="${jdbc.password}"></property>
        </bean>
        <!--2. 配置SqlSessionFactoryBean-->
        <bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sessionFactory">
            <!--注入dataSource-->
            <property name="dataSource" ref="dataSource"></property>
            <!--注入要设置别名的包-->
            <property name="typeAliasesPackage" value="com.liu2m.pojo"></property>
            <!--name="configLocation"可以注入核心配置文件的路径-->
        </bean>
        <!--3. 配置MapperScannerConfigurer-->
        <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" id="scannerConfigurer">
            <!--注入映射配置文件所在的包
            各种Dao代理对象的id就是接口名首字母小写-->
            <property name="basePackage" value="com.liu2m.dao"></property>
        </bean>
        <!--spring整合mybatis end-->
    </beans>
    
  3. 在Spring配置文件中导入整合mybatis的配置文件:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
        <!--包扫描:base-package指定要扫描注解的包-->
        <context:component-scan base-package="com.liu2m"></context:component-scan>
        <!--导入整合mybatis的配置文件-->
        <import resource="classpath:applicationMyBatis.xml"></import>
    </beans>
    
  4. 使用:

    @Test
    public void test01() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        // 从核心容器中获取PojoDao的代理对象
        PojoDao pojoDao = (PojoDao) applicationContext.getBean("pojoDao");
        // 使用pojoDao
        System.out.println(pojoDao.findAll());
    }
    

导入坐标后,使用纯注解来整合-:

  1. 创建MyBatis配置类:

    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.mapper.MapperScannerConfigurer;
    import org.springframework.context.annotation.Bean;
    import org.springframework.jdbc.datasource.DriverManagerDataSource;
    
    import javax.sql.DataSource;
    
    public class MyBatisConfig {
        @Bean
        public DataSource getDataSource() {
            DriverManagerDataSource dataSource = new DriverManagerDataSource();
            dataSource.setDriverClassName("com.mysql.jdbc.Driver");
            dataSource.setUrl("jdbc:mysql://localhost:3306/db01?useSSL=false");
            dataSource.setUsername("root");
            dataSource.setPassword("123456");
            return dataSource;
        }
    
        @Bean
        public SqlSessionFactoryBean getSqlSessionFactoryBean(DataSource dataSource) {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dataSource);
            sqlSessionFactoryBean.setTypeAliasesPackage("com.liu2m.pojo");
            return sqlSessionFactoryBean;
        }
    
        @Bean
        public MapperScannerConfigurer getMapperScannerConfigurer() {
            MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
            mapperScannerConfigurer.setBasePackage("com.liu2m.dao");
            return mapperScannerConfigurer;
        }
    }
    
  2. 创建Spring配置类:

    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Import;
    
    // 创建一个配置类
    @Configuration
    @ComponentScan(basePackages = "com.liu2m")
    @Import({MyBatisConfig.class})
    public class SpringConfig {
    }
    
  3. 使用:

    @Test
    public void test01() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        // 从核心容器中获取PojoDao的代理对象
        PojoDao pojoDao = (PojoDao) applicationContext.getBean("pojoDao");
        // 使用pojoDao
        System.out.println(pojoDao.findAll());
    }
    

8.3 Spring整合JUnit-

整合后可以不用创建核心容器,自动注入使用:junit的版本必须是4.12及以上

import com.liu2m.dao.PojoDao;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class SpringTest {
    // 自动注入要使用的对象
    @Autowired
    private PojoDao pojoDao;

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

推荐阅读更多精彩内容