Spring---使用篇

·  阅读 42
Spring---使用篇

Spring介绍

Spring 是一个开源框架,翻译中文就是春天,于2004年由 Rod Johnson创建,Spring 给Java 程序员带来了春天,是为了解决企业应用开发的复杂性而诞生的。

目前是 JavaEE 开发的灵魂框架,他可以简化 JavaE E开发,可以非常方便整合其他框架,无侵入式的进行功能增强。

Spring 的核心就是 控制反转(IOC)和面向切面(AOP)

IOC控制反转

在 JavaWeb 阶段中的业务代码中,新的需求提出,我们需要根据需求去修改源码,如果程序很庞杂,因为业务对象之间的 耦合性 太高了,修改一次可能会导致严重的 bug 或者对象之间的依赖问题,从而牵一发而动全身。

举个例子:普通人生病了,控制权在自己手里,自己可以打针吃药,但是普通人没有医学知识啊,一不小心医死了咋办?程序员自己创建管理对象就容易出问题啊,普通人的控制权被医院的护士夺走了,控制权就反转到护士手中,普通人只需要躺在病床上等待护士的打针喂药,这些护士都帮你弄好安排好了而且非常专业,普通人的病自然就好了。 程序员依赖注入和创建对象的控制权反转给了容器,通过容器来实现对象的创建、管理、装配,这就是 IOC 。

耦合的代码:

OIP-C (1).jpeg

解耦的代码:

OIP-C.jpeg

控制反转 IOC 是一种设计思想, DI (依赖注入) 是实现 IOC 的一种方式。这种思想,从本质上解决了程序过于耦合的问题,我们程序猿不再需要去管理对象的创建了,系统的耦合大大降低,可以更加专注于业务的实现上。

光速入门

【1】导入依赖

导入 SpringIOC 相关依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.9.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
</dependency>


复制代码

【2】编写配置文件

在 resources 目录下创建 applicationContext.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
		
  	 <!--
        classs: 配置类的全类名
        id: 配置一个唯一标识
     -->
    <bean id="userDao" class="com.nhb.dao.impl.UserDaoImpl"/>
</beans>
复制代码

【3】创建容器并获取对象

public static void main(String[] args) {
  	//创建Spring容器,指定要读取的配置文件路径
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    //通过id从容器中获取对象
    UserDaoImpl userDao = (UserDaoImpl) context.getBean("userDao");
    //调用对象的方法进行测试
    userDao.getUserById(1);
}
复制代码

Bean标签的属性

id

bean 的唯一标识,同一个 Spring 容器中不允许重复

class

全类名,用于反射创建对象

scope

scope 主要有两个值:singleton 和 prototype

  • 如果设置为 singleton 则一个容器中只会有这个一个 bean 对象,默认容器创建的时候就会创建该对象。

  • 如果设置为 prototype 则一个容器中会有多个该 bean 对象,每次调用 getBean 方法获取时都会创建一个新对象。

  • 总的来说 singleton 是单列模式默认开启,prototype 是非单例模式

DI依赖注入

依赖注入可以理解成 IOC 的一种应用场景,反转的是对象间依赖关系维护权。

依赖:bean 对象的创建依赖于容器

注入:bean 对象中的所有属性,由容器注入

set注入

大前提:要注入的 JavaBean 需要对外提供setter方法,不然无法注入!!!

【1】实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    private String name;
    private Address address;
    private String[]  books;
    private List<String> hobbys;
    private Map<String, String> card;
    private Properties info;
}

public class Address {
}
复制代码

【2】xml配置

<bean id="address" class="com.nhb.pojo.Address"/>

<bean id="student" class="com.nhb.pojo.Student">

    <!--  普通值注入,value-->
    <property name="name" value="张三"/>

    <!-- bean注入,ref-->
    <property name="address" ref="address"/>

    <!-- 数组注入,ref-->
    <property name="books">
        <list>
            <value>篮球</value>
            <value>rap</value>
            <value>唱跳</value>
        </list>
    </property>

    <!-- Map注入-->
    <property name="card">
        <map>
            <entry key="手机号" value="19187806381"/>
            <entry key="身份证" value="182938473940284930"/>
        </map>
    </property>

    <!-- Properties注入-->
    <property name="info">
        <props>
            <prop key="url">127.0.0.1</prop>
        </props>
    </property>
</bean>
复制代码

有参构造注入

大前提:要注入的 JavaBean 需要对外提供有参构造器方法,不然无法注入!!!

【1】实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;
    private String name;
    private String password;
}
复制代码

【2】xml配置

<!--使用有参构造进行注入-->
<bean class="com.nhb.pojo.Student" id="student2" >
    <constructor-arg name="id" value="1"/>
    <constructor-arg name="name" value="zs"/>
    <constructor-arg name="password" value="123456"/>
</bean>
复制代码

配置文件

我们让 Spring 可以读取 properties 文件中的 key/value,然后使用其中的值。

读取properties文件

【1】引入druid依赖

<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.12</version>
</dependency>
复制代码

【2】定义properties属性

jdbc.url = jdbc:mysql://主机ip:3306/demo?useSSL=false
jdbc.driver = com.mysql.cj.jdbc.Driver
jdbc.username = ****
jdbc.password = ****
复制代码

【3】xml配置

  • 注意:context 命名空间的引入是否正确

  • classpath 表示类加载路径下。

  • classpath:*.properties 其中的 * 表示文件名任意。

<?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 https://www.springframework.org/schema/context/spring-context.xsd">

    <!--    指定要读取的文件的路径-->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
      
        <!-- 使用${key}来表示具体的值,注意要再value属性中使用才可以-->
        <property name="driverClassName" value="${jdbc.url}"/>
        <property name="url" value="${jdbc.driver}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

</beans>
复制代码

这一套操作下来,我们把 druid 配置好并放入IOC容器中了,想使用的时候就可以从容器中获取 druid的对象。

引入其他xml配置文件

当项目做大的时候,配置文件过多,我们可以在主的配置文件中通过 import 标签的 resource 属性,引入其他的xml配置文件

<import resource="classpath:applicationContext-login.xml"/>
复制代码

Spring注解开发

为了简化配置,Spring 支持使用注解代替 xml 配置,随着 SpringBoot 的流行,注解开发已经成为了主流。

如果要使用注解开发必须要开启组件扫描,这样加了注解的类才会被识别出来。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
       https://www.springframework.org/schema/context/spring-context.xsd">

      <!--启动组件扫描,指定对应扫描的包路径,该包及其子包下所有的类都会被扫描,加载包含指定注解的类-->
      <context:component-scan base-package="com.nhb"/>

</beans>
复制代码

IOC相关的注解

  • @Component:**其他类 **可以使用

  • @Controller:Controllerl类 要求使用

  • @Service:Service类 要求使用

  • @Repository:Dao类 要求使用

上述4个注解都是加到类上的。

他们都可以起到类似 bean 标签的作用,可以把加了该注解类的对象放入 Spring 容器中。

实际再使用时选择任意一个都可以,但是后3个注解是语义化注解。

例如:

@Repository("userDao")
public class UserDaoImpl implements UserDao {
    @Override
    public void getUserById(Integer id) {
        System.out.println(id);
    }
}
复制代码
@Service("userService")
public class UserServiceImpl implements UserService {
    @Override
    public void getUserById(Integer id) {
        System.out.println(id);
    }
}
复制代码

等价于:

<?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="com.nhb.dao.impl.UserDaoImpl" id="userDao"/>
        <bean class="com.nhb.service.impl.UserServiceImpl" id="userService"/>

</beans>
复制代码

测试:

public static void main(String[] args) {
    ApplicationContext context = new
            //创建Spring容器,指定要读取的配置文件路径
            ClassPathXmlApplicationContext("applicationContext.xml");
    //通过id从容器中获取对象

    //Dao
    UserService userService = (UserService) context.getBean("userService");
    userService.getUserById(1);

    //Service
    UserDao userDao = (UserDao) context.getBean("userDao");
    userDao.getUserById(2);

}
复制代码

DI相关注解

@Value

主要用于 String、Integer 等可以直接赋值的属性注入,不依赖setter方法。

例如:

@ToString
@Service("userService")
public class UserServiceImpl implements UserService {

    @Value("100")
    private Integer sum;
  
    @Value("张三")
    private String name;

    @Override
    public void getUserById(Integer id) {
        System.out.println(id);
    }
}
复制代码

等价于:

<!--  普通值注入,value-->
<property name="name" value="张三"/>
复制代码

@AutoWired

Spring 会给加了该注解的属性自动注入数据类型相同的对象。

例如:

@Service("userService")
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Override
    public void getUserById(Integer id) {
        userDao.getUserById(id);
    }
}
复制代码

等价于:

<!-- bean注入,ref-->
<property name="address" ref="address"/>
复制代码

@Qualifier

如果相同类型的 bean 在容器中有多个时,单独使用 @AutoWired 就不能满足要求,这时候可以再加上@Qualifier 来指定 bean 的名字从容器中获取 bean 注入。

例如:

@Service("userService")
public class UserServiceImpl implements UserService {

    @Autowired
    @Qualifier("userDao11")
    private UserDao userDao;

    @Override
    public void getUserById(Integer id) {
        userDao.getUserById(id);
    }
}
复制代码

注意:该注解直接不能单独使用,单独使用没有作用

xml配置文件相关注解

@Configuration

标注在类上,表示当前类是一个配置类。我们可以用注解类来完全替换掉xml配置文件。

例如:

@Configuration
public class ApplicationConfig {
  
}
复制代码

等价于:

<?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">

</beans>
复制代码

注意:如果使用配置类替换了 xml 配置,spring 容器要使用:AnnotationConfigApplicationContext

@ComponentScan

标注在类上,可以用来代替 context:component-scan 标签来配置组件扫描,basePackages 属性来指定要扫描的包。

例如:

@Configuration
@ComponentScan(basePackages = "com.nhb")//指定要扫描的包
public class ApplicationConfig {
  
}
复制代码

等价于:

<!--启动组件扫描,指定对应扫描的包路径,该包及其子包下所有的类都会被扫描,加载包含指定注解的类-->
<context:component-scan base-package="com.nhb"/>
复制代码

@Bean

可以用来代替 bean 标签,主要用于第三方类的注入。使用:定义一个方法,在方法中创建对应的对象并且作为返回值返回,然后在方法上加上 @Bean 注解,注解的 value 属性来设置 bean 的名称。

例如:

@Configuration
@ComponentScan(basePackages = "com.nhb")
public class ApplicationConfig {

    @Bean("dataSource")
    public DruidDataSource getDataSource(){
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
        druidDataSource.setUrl("jdbc:mysql://主机ip:3306/mybatis_db");
        druidDataSource.setUsername("***");
        druidDataSource.setPassword("***");
        return druidDataSource;
    }

}
复制代码

等价于:

<bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
    <!-- 使用${key}来表示具体的值,注意要再value属性中使用才可以-->
    <property name="driverClassName" value="${jdbc.url}"/>
    <property name="url" value="${jdbc.driver}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>
复制代码

@PropertySource

可以用来代替 context:property-placeholder,让 Spring 读取指定的 properties 文件。然后可以使用 @Value 来获取读取到的值。

例如:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis_db
jdbc.username=root
jdbc.password=root
复制代码

读取文件并且获取值

@Configuration
@ComponentScan(basePackages = "com.sangeng")
@PropertySource("jdbc.properties")
public class ApplicationConfig {

    @Value("${jdbc.driver}")
    private String driverClassName;
  
    @Value("${jdbc.url}")
    private String url;
  
    @Value("${jdbc.username}")
    private String username;
  
    @Value("${jdbc.password}")
    private String password;


    @Bean
    public DruidDataSource getDataSource(){
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName(driverClassName);
        druidDataSource.setUsername(username);
        druidDataSource.setUrl(url);
        druidDataSource.setPassword(password);
        return druidDataSource;
    }

}
复制代码

注意:使用 @Value 获取读到的 properties 文件中的值时使用的是 ${key},而不是#{key}。

如何选择?

SSM:

  • xml 用来管理 bean

  • 注解只负责完成属性注入

SpringBoot:

  • 纯注解

AOP

面向切面编程,通过 AOP 我们可以在保证原有业务代码不变的情况下,添加额外的动作,比如我们的某些方法执行完成之后,需要打印日志,那么这个时候,我们就可以使用 AOP 来帮助我们完成,它可以批量地为这些方法添加动作,可以说,它相当于将我们原有的方法,在不改变源代码的基础上进行了增强处理,这么一看它的底层原理就是通过动态代理实现的。

光速入门

【1】导入依赖

<!--SpringIOC相关依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.9.RELEASE</version>
</dependency>

<!--AOP相关依赖-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.13</version>
</dependency>
复制代码

【2】开启组件扫描

<context:component-scan base-package="com.nhb"/>
复制代码

【3】将bean 注入容器中

@Service("userService")
public class UserServiceImpl implements UserService  {
    
    @Override
    public void add() {
        System.out.println("添加用户");
    }
}
复制代码

【4】开启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
       https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       https://www.springframework.org/schema/aop/spring-aop.xsd">

      <!--启动组件扫描,指定对应扫描的包路径,该包及其子包下所有的类都会被扫描,加载包含指定注解的类-->
      <context:component-scan base-package="com.nhb"/>

      <!--开启aop注解支持-->
       <aop:aspectj-autoproxy/>

</beans>
复制代码

【5】创建切面类

创建一个类,在类上加上 @Component 和 @Aspect ,使用 @Pointcut 注解来指定要被增强的方法。

使用 @Before 注解来给我们的增强代码所在的方法进行标识,并且指定了增强代码是在被增强方法执行之前执行的。

例如:

@Component
@Aspect
public class MyAop {
		
    //用Pointcut注解中的属性来指定对哪些方法进行增强
    @Pointcut("execution(* com.nhb.service.impl.*.*(..))")
    public void pt(){}
    
  	/*
  	* 用@Before注解来指定该方法中是增强的代码,并且是在被增强方法执行前执行的
  	* @Before的属性写上加了@Pointcut注解的方法: 方法名()
  	*/
    @Before("pt()")
    public void methodBefore(){
        System.out.println("方法被调用了");
    }
}
复制代码

【6】测试

public static void main(String[] args) {
    ApplicationContext context = new
            //创建Spring容器,指定要读取的配置文件路径
            ClassPathXmlApplicationContext("applicationContext.xml");
    //通过id从容器中获取对象

    UserService userService = (UserService) context.getBean("userService");
    userService.add();
}
复制代码

AOP核心概念

  • Joinpoint(连接点):所谓连接点是指那些可以被增强到的点,在 spring 中,这些点指的是方法,因为spring 只支持方法类型的连接点

  • Pointcut(切入点):所谓切入点是指被增强的连接点(方法)

  • Advice(通知 / 增强):所谓通知是指具体增强的代码

  • Target(目标对象):被增强的对象就是目标对象

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

  • Proxy (代理):一个类被 AOP 增强后,就产生一个结果代理类

切点确定

切点表达式

可以使用切点表达式来表示要对哪些方法进行增强。

写法:execution([修饰符] 返回值类型 包名.类名.方法名(参数))

  • 访问修饰符可以省略,大部分情况下省略
  • 返回值类型、包名、类名、方法名可以使用星号 * 代表任意
  • 包名与类名之间一个点 . 代表当前包下的类,两个点 .. 表示当前包及其子包下的类
  • 参数列表可以使用两个点 .. 表示任意个数,任意类型的参数列表

例如:

//表示com.nhb.service包下任意类,方法名任意,参数列表任意,返回值类型任意
execution(* com.nhb.service.*.*(..))

//表示com.nhb.service包及其子包下任意类,方法名任意,参数列表任意,返回值类型任意
execution(* com.nhb.service..*.*(..))   

//表示com.nhb.service包下任意类,方法名任意,要求方法不能有参数,返回值类型任意
execution(* com.nhb.service.*.*())     

//表示com.nhb.service包下任意类,要求方法不能有参数,返回值类型任意,方法名要求已delete开头
execution(* com.nhb.service.*.delete*(..))     
复制代码

切点函数@annotation

我们也可以在要增强的方法上加上注解,然后使用 @annotation 来表示对加了什么注解的方法进行增强。

写法:@annotation(注解的全类名)

例如:

自定义注解类:

@Target({ElementType.METHOD})//该注解可以加在方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface InvokeLog {
}
复制代码

给需要增强的方法增加注解

@Service("userService")
public class UserServiceImpl implements UserService  {

    @Override
    @InvokeLog
    public void add() {
        System.out.println("添加用户");
    }
}
复制代码

切面类中使用 @annotation 来确定要增强的方法:

@Component
@Aspect
public class MyAop {

    @Pointcut("@annotation(com.nhb.aop.InvokeLog)")
    public void pt(){}

    @Before("pt()")
    public void methodBefore(){
        System.out.println("方法被调用了");
    }
}
复制代码

通知分类

  • @Before:前置通知,在目标方法执行前执行

  • @AfterReturning: 返回后通知,在目标方法执行后执行,如果出现异常不会执行

  • @After:后置通知,在目标方法之后执行,无论是否出现异常都会执行

  • @AfterThrowing:异常通知,在目标方法抛出异常后执行

  • @Around:环绕通知,围绕着目标方法执行

伪代码:

public Object test() {
  
    before();//@Before 前置通知
  
    try {
        Object ret = 目标方法();//目标方法调用
        afterReturing();//@AfterReturning 返回后通知
    } catch (Throwable throwable) {
        throwable.printStackTrace();
        afterThrowing();//@AfterThrowing 异常通知通知
    }finally {
        after();//@After 后置通知
    }
    return ret;
}
复制代码

环绕通知非常特殊,它可以对目标方法进行全方位的增强。

@Component
@Aspect
public class MyAop {

    @Pointcut("@annotation(com.nhb.aop.InvokeLog)")
    public void pt(){}

    @Around("pt()")
    public void around(ProceedingJoinPoint pjp){
      
        System.out.println("目标方法前");

        try {
            //目标方法执行
            pjp.proceed();
            System.out.println("目标方法后");
          
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("目标方法出现异常");
          
        }finally {
            System.out.println("finally中进行增强");
        }
    }
}
复制代码

实际开发过程中 @Around 是最好用的

获取被增强方法相关信息

我们实际对方法进行增强时往往还需要获取到被增强代码的相关信息,比如方法名,参数,返回值,异常对象等。

直接在环绕通知方法中增加一个 ProceedingJoinPoint类型 的参数,这个参数封装了被增强方法的相关信息,该参数的 proceed() 方法被调用相当于被增强方法被执行,调用后的返回值就相当于被增强方法的返回值。

例如:

@Service("userService")
public class UserServiceImpl implements UserService  {

    @Override
    @InvokeLog
    public String add(Integer id) {
        return "添加用户" + id;
    }
}
复制代码

切面类

@Component
@Aspect
public class MyAop {

    @Pointcut("@annotation(com.nhb.aop.InvokeLog)")
    public void pt(){}

    @Around(value = "pt()")
    public void around(ProceedingJoinPoint pjp) {

        //方法调用时传入的参数
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));

        //被代理对象
        Object target = pjp.getTarget();
        System.out.println(target);

        //获取被被增强方法签名封装的对象
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        System.out.println(signature);

        try {
            //ret就是目标方法执行后的返回值
            Object ret = pjp.proceed();
            System.out.println(ret);

        } catch (Throwable throwable) {
            //throwable就是出现异常时的异常对象
            throwable.printStackTrace();
        }
    }
}
复制代码

注意:目标方法有返回值,我们都环绕通知必须也得有返回值,返回的值是 proceed() 方法执行的结果。

多切面顺序问题

在实际项目中我们可能会存在配置了多个切面的情况,这种情况下我们很可能需要控制切面的顺序。

我们在默认情况下 Spring 有它自己的排序规则(按照类名排序),默认排序规则往往不符合我们的要求,我们需要进行特殊控制。

如果是注解方式配置的 AOP 可以在切面类上加 @Order注解 来控制顺序,@Order 中的属性越小优先级越高。

如果是 XML 方式配置的 AOP,可以通过调整 配置顺序 来控制。

例如:

@Component
@Aspect
@Order(2)
public class APrintLogAspect {
    //省略无关代码
}


@Component
@Aspect
@Order(1)
public class CryptAspect {
    //省略无关代码
}
复制代码

SpringAOP原理

实际上 SpringAOP 其实底层就是使用动态代理来完成的,并且使用了两种动态代理分别是 JDK 的动态代理和 Cglib 动态代理。

JDK动态代理

JDK的动态代理使用的 java.lang.reflect.Proxy 这个类来进行实现的,要求被代理(被增强)的类需要实现接口,并且 JDK 动态代理也只能对接口中的方法进行增强。

JDK实现动态代理需要使用到两个类:

  1. Proxy:代理

  2. InvocationHandler:调用程序

【1】定义接口

public interface UserService {
    void add();
}
复制代码

【2】真实角色

public class UserServiceImpl implements UserService{
    @Override
    public void add() {
        System.out.println("添加用户");
    }
}
复制代码

【3】动态代理类

public class ProxyInvocationHandler implements InvocationHandler {

    //被代理的接口
    private Object target;

    public void setTarget(Object target) {
        this.target = target;
    }

    //生成得到代理类
    public Object getProxy(){
        return Proxy.newProxyInstance(this.getClass().getClassLoader(),
                target.getClass().getInterfaces(),this);
    }

    //处理代理实例,并返回结果
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = method.invoke(target, args);
        //调用日志方法
        log(method.getName());
        return result;
    }

    //日志方法
    public void log(String msg){
        System.out.println("【日志】执行" + msg + "方法");
    }
}
复制代码

【4】客户类访问动态代理类

public class Client {
    public static void main(String[] args) {
        //真实角色
        UserService userService = new UserServiceImpl();

        //代理角色,不存在
        ProxyInvocationHandler pih = new ProxyInvocationHandler();

        //设置要代理的对象
        pih.setTarget(userService);

        //动态生成代理类
        UserService proxy = (UserService) pih.getProxy();

        proxy.add();

    }
}
复制代码

Cglib动态代理

使用的是 org.springframework.cglib.proxy.Enhancer 类进行实现的。

public static void main(String[] args) {
  Enhancer enhancer = new Enhancer();
  
  //设置父类的字节码对象
  enhancer.setSuperclass(AIControllerImpl.class);
  
  enhancer.setCallback(new MethodInterceptor() {
      //使用代理对象执行方法是都会调用到intercept方法
      @Override
      public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
          //判断当前调用的方法是不是getAnswer方法 如果是进行增强
          if ("getAnswer".equals(method.getName())){
              System.out.println("被增强了");
          }
          //调用父类中对应的方法
          Object ret = methodProxy.invokeSuper(o, objects);
          return ret;
      }
  });
  
  //生成代理对象
  AIControllerImpl proxy = (AIControllerImpl) enhancer.create();
  
  //System.out.println(proxy.getAnswer("你好吗?"));
  System.out.println(proxy.fortuneTelling("你好吗?"));
}
复制代码

总结

  • JDK 动态代理要求被代理(被增强)的类必须要实现接口,生成的代理对象相当于是被代理对象的兄弟。

  • Cglib 的动态代理不要求被代理(被增强)的类要实现接口,生成的代理对象相当于被代理对象的子类对象。

  • SpringAOP默认情况下优先使用的是 JDK 的动态代理,如果使用不了JDK的动态代理才会使用Cglib的动态代理。

Spring整合Junit

【1】导入依赖

<!-- junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

<!-- spring整合junit的依赖 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.1.9.RELEASE</version>
</dependency>
复制代码

【2】编写测试类

在测试类上加上:

@RunWith(SpringJUnit4ClassRunner.class) 注解,指定让测试运行于Spring环境

@ContextConfiguration注解,指定Spring容器创建需要的配置文件或者配置类

//让测试运行与Spring测试环境
@RunWith(SpringJUnit4ClassRunner.class)

//设置Spring配置文件或者配置类
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class MyTest {
    
}
复制代码

【3】注入对象进行测试

在测试类中注入要测试的对象,定义测试方法,在其中使用要测试的对象。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class MyTest {

    //想测哪个对象,就注入哪个对象
    @Autowired
    private UserService userService;

    //定义测试方法
    @Test
    public void testUserService() {
        userService.add(10);
    }
}
复制代码

Spring整合Mybatis

我们如果想把 Mybatis 整合到 Spring 中需要使用一个整合包 mybatis-spring

官方文档:mybatis.org/spring/zh/i…

【1】导入依赖

<!-- spring-jdbc -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.1.9.RELEASE</version>
</dependency>

<!-- mybatis整合到Spring的整合包 -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.4</version>
</dependency>

<!-- mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.10</version>
</dependency>

<!-- mysql -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.11</version>
</dependency>

<!-- druid数据源 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.16</version>
</dependency>
复制代码

【2】注入整合相关对象

<?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
   https://www.springframework.org/schema/context/spring-context.xsd">

    <!--读取properties文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!--创建连接池注入容器-->
    <bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
            <property name="driverClassName" value="${jdbc.driver}"/>
    </bean>

    <!--spring整合mybatis后控制的创建获取SqlSessionFactory的对象-->
    <bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sessionFactory">
            <!--配置连接池-->
            <property name="dataSource" ref="dataSource"/>
            <!--配置mybatis配置文件的路径-->
            <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>

    <!--mapper扫描配置,扫描到的mapper对象会被注入Spring容器中-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" id="mapperScannerConfigurer">
            <property name="basePackage" value="com.nhb.dao"/>
    </bean>

</beans>
复制代码

【3】mybatis配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!--  关联xml配置的文件  -->
    <mappers>
        <package name="com.nhb.dao"/>
    </mappers>

</configuration>
复制代码

【4】获取Mapper对象进行使用

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class MyTest {

    //想测哪个对象,就注入哪个对象
    @Autowired
    private UserDao userDao;

    //定义测试方法
    @Test
    public void testUserService() {
        User user = userDao.getUserById(6);
        System.out.println(user);
    }
}
复制代码

Spring声明式事务

事务的概念

​ 保证一组数据库的操作,要么同时成功,要么同时失败

四大特性

  • 隔离性

    • 多个事务之间要相互隔离,不能互相干扰
  • 原子性

    • 指事务是一个不可分割的整体,类似一个不可分割的原子
  • 一致性

    • 保障事务前后这组数据的状态是一致的。要么都是成功的,要么都是失败的。
  • 持久性

    • 指事务一旦被提交,这组操作修改的数据就真的的发生变化了,即使接下来数据库故障也不应该对其有影响。

实现声明式事务

如果我们自己去对事务进行控制的话我们就需要值原来核心代码的基础上加上事务控制相关的代码,而在我们的实际开发中这种事务控制的操作也是非常常见的,所以 Spring 提供了声明式事务的方式让我们去控制事务。

只要简单的加个注解 (或者是xml配置) 就可以实现事务控制,不需要事务控制的时候只需要去掉相应的注解即可。

【1】准备数据

USE `demo`;
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(50) DEFAULT NULL,
  `money` DOUBLE DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT  INTO `account`(`id`,`name`,`money`) VALUES (1,'zs',100),(2,'lis',100);
复制代码

【2】创建Service和Dao

public interface AccountService {
    /**
     * 转账
     * @param outId 转出账户的id
     * @param inId 转出账户的id
     * @param money 转账金额
     */
    public void transfer(Integer outId,Integer inId,Double money);
}


@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccoutDao accoutDao;

    @Override
    public void transfer(Integer outId, Integer inId, Double money) {
        //增加
        accoutDao.updateMoney(inId,money);
        //减少
        accoutDao.updateMoney(outId,-money);
    }
}

@Mapper
public interface AccoutDao {
    void updateMoney(@Param("id") Integer id,@Param("updateMoney") Double updateMoney);
}
复制代码

AccoutDao.xml如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.nhb.dao.AccoutDao">

    <update id="updateMoney">
        update account set money = money + #{updateMoney} where id = #{id}
    </update>
</mapper>
复制代码

【3】配置事务管理器和事务注解驱动

在 Spring 的配置文件中添加如下配置:

<!--把事务管理器注入Spring容器,需要配置一个连接池-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<!--开启事务注解驱动,配置使用的事务管理器-->
<tx:annotation-driven transaction-manager="txManager"/>
复制代码

【4】添加注解

在需要进行事务控制的方法或者类上添加 @Transactional 注解就可以实现事务控制。

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccoutDao accoutDao;

    @Transactional
    @Override
    public void transfer(Integer outId, Integer inId, Double money) {
        //增加
        accoutDao.updateMoney(inId,money);

        //模拟异常
        System.out.println(1/0);

        //减少
        accoutDao.updateMoney(outId,-money);
    }
}
复制代码

注意:如果加在类上,这个类的所有方法都会受事务控制,如果加在方法上,就是那一个方法受事务控制,因为声明式事务底层是通过 AOP 实现的,所以最好把 AOP 相关依赖都加上

属性配置

当事务方法嵌套调用时,需要控制是否开启新事务,可以使用事务传播行为来控制。

事务传播行为propagation

问题:当转账成功了,但是日志出现了异常,导致全部回滚,转账是核心业务,既然都转账成功了,这一条日志不记录也罢

测试案例:

@Service
public class TestServiceImpl {
    @Autowired
    AccountService accountService;

    @Transactional
    public void test(){
        accountService.transfer(1,2,10D);
        accountService.log();
    }
}

public class AccountServiceImpl implements AccountService {
	  //...省略其他不相关代码
  
  	@Transactional
    public void transfer(Integer outId, Integer inId, Double money) {
      //增加
      accoutDao.updateMoney(inId,money);
      //减少
      accoutDao.updateMoney(outId,-money);
    }
  
    @Transactional
    public void log() {
        System.out.println("打印日志");
        int i = 1/0;
    }
}
复制代码
属性值行为
REQUIRED(必须要有)外层方法有事务,内层方法就加入。外层没有,内层就新建
REQUIRES_NEW(必须要有新事务)外层方法有事务,内层方法新建。外层没有,内层也新建
SUPPORTS(支持有)外层方法有事务,内层方法就加入。外层没有,内层就也没有
NOT_SUPPORTED(支持没有)外层方法有事务,内层方法没有。外层没有,内层也没有
MANDATORY(强制要求外层有)外层方法有事务,内层方法加入。外层没有。内层就报错
NEVER(绝不允许有)外层方法有事务,内层方法就报错。外层没有。内层就也没有

例如:

public class AccountServiceImpl implements AccountService {
	  //...省略其他不相关代码
  
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void transfer(Integer outId, Integer inId, Double money) {
      //增加
      accoutDao.updateMoney(inId,money);
      //减少
      accoutDao.updateMoney(outId,-money);
    }
  
    @Transactional
    public void log() {
        System.out.println("打印日志");
        int i = 1/0;
    }
}
复制代码

隔离级别isolation

  • Isolation.DEFAULT 使用数据库默认隔离级别
  • Isolation.READ_UNCOMMITTED
  • Isolation.READ_COMMITTED
  • Isolation.REPEATABLE_READ
  • Isolation.SERIALIZABLE
@Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.READ_COMMITTED)
public void transfer(Integer outId, Integer inId, Double money) {
    //增加
    accoutDao.updateMoney(inId,money);
    //减少
    accoutDao.updateMoney(outId,-money);
}
复制代码

只读readOnly

如果事务中的操作都是读操作,没涉及到对数据的写操作可以设置 readOnly为true,这样可以提高效率。

@Transactional(readOnly = true)
    public void log() {
        System.out.println("打印日志");
        int i = 1/0;
    }
复制代码
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改