Spring详解 --持续更新

x33g5p2x  于2022-03-06 转载在 其他  
字(15.7k)|赞(0)|评价(0)|浏览(275)

本文素材来自动力结点王妈妈的课程

什么是Spring

Spring 是于 2003 年兴起的一个轻量级的 Java 开发框架,它是为了解 决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IoC)和面向切面编程(AOP)。Spring 根据代码的功能特点,使用 Ioc 降低业务对象之间耦合度。通过 Spring 提供的 AOP 功能,方便进行面向切面的编程。

Spring官网

IoC控制反转

控制反转(IoC,Inversion of Control),是一种思想。指将传统上由程序代码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理。Spring 框架使用依赖注入(DI)实现 IoC。

初识Spring

1、创建maven项目

2、引入 maven 依赖

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

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>

3、定义接口与实体类

public interface SomeService {
    void doSome();
}

public class SomeServiceImpl implements SomeService {
    public SomeServiceImpl() {
        super();
        System.out.println("SomeServiceImpl无参数构造方法");
    }
    @Override
    public void doSome() {
        System.out.println("doSome业务方法");
    }
}

4、创建 Spring 配置文件
在 src/main/resources/目录现创建一个 xml 文件,文件名可以随意,但Spring 建议的名称为applicationContext.xml。注意要将resources文件将指定为资源文件夹(这个文件夹中的文件编译之后都会放在类路径下),如何指定?
右键文件夹 → Mark Directory as → Resources Root

<?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 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!--
        注册bean对象
            id:自定义对象的名称
            class:类的全限定名称,不能是接口
    -->
    <bean id="someService" class="com.why.SomeServiceImpl"></bean>
</beans>

5、定义测试类

public class MyTest {
    @Test
    public void test01(){
        //指定spring配置文件的位置和名称
        String resource = "applicationContext.xml";
        //创建spring容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
        //使用id从spring容器中获取对象
        SomeService someService = (SomeService) ac.getBean("someService");
        //执行对象的方法
        someService.doSome();
    }
}

基于XML依赖注入(DI)

set注入

public class School {
    private String schoolName;
    private String addr;
	/*setter*/
	/*toString*/
}

public class Student {
    private String name;
    private int age;
    private School school;
	/*setter*/
	/*toString*/
}
<!--使用set方法注入参数-->
    <bean id="mySchool" class="com.why.School">
        <!--简单类型-->
        <property name="schoolName" value="浙江中医药大学"></property>
        <property name="addr" value="浙江杭州"></property>
    </bean>

    <bean id="zs" class="com.why.Student">
        <property name="name" value="张三"></property>
        <property name="age" value="20"></property>
        <!--引用类型-->
        <property name="school" ref="mySchool"></property>
    </bean>

构造注入

//Student构造函数
public Student(String stuName, int stuAge, School stuSchool) {
	this.name = stuName;
    this.age = stuAge;
    this.school = stuSchool;
}
<!--构造注入-->
    <bean id="ls" class="com.why.Student">
        <constructor-arg name="stuName" value="李四"></constructor-arg>
        <constructor-arg name="stuAge" value="20"></constructor-arg>
        <constructor-arg name="stuSchool" ref="mySchool"></constructor-arg>
    </bean>
    <!--index:指明该参数对应着构造器的第几个参数,从0开始(少用)-->
	<bean id="ww" class="com.why.Student">
        <constructor-arg index="0" value="王五"></constructor-arg>
        <constructor-arg index="1" value="20"></constructor-arg>
        <constructor-arg index="2" ref="mySchool"></constructor-arg>
    </bean>
    <!--index属性不要也行,但要注意:若参数类型相同或之间有包含关系,则需要保证赋值顺序要与构造器中的参数顺序一致-->
    <bean id="zl" class="com.why.Student">
        <constructor-arg value="赵六"></constructor-arg>
        <constructor-arg value="20"></constructor-arg>
        <constructor-arg ref="mySchool"></constructor-arg>
    </bean>

引用类型属性自动注入

byName方式自动注入

容器是通过调用者的 bean 类的属性名与配置文件的被调用者 bean 的id 进行比较而实现自动注入的

public class Student {
    private String name;
    private int age;
    private School schoolXXX;
	/*setter*/
	/*toString*/
}

byName方式自动注入时Spring会根据用于类型的变量名去调用相应的setter方法
(比如这里我们要注入zy对象 Spring根据autowire的属性发现是根据byName方式自动注入。Spring会找到Student中的Schoo引用类型,根据schoolXXX属性名在容器中找到名为schoolXXX的对象(如果在找不到schoolXXX对象Spring就会去找引用类型名首字母小写的对象本例中为school 如果两个都找不到就会报错)然后去调用setSchoolXXX方法将引用类型注入)

<bean id="schoolXXX" class="com.why.School">
        <property name="schoolName" value="温州大学"></property>
        <property name="addr" value="浙江温州"></property>
    </bean>

    <bean id="zy" class="com.why.Student" autowire="byName">
        <property name="name" value="张扬"></property>
        <property name="age" value="20"></property>
    </bean>

byType方式自动注入

使用 byType 方式自动注入,要求:配置文件中被调用者 bean 的 class 属性指定的类,要与代码中调用者 bean 类的某引用类型属性类型同源。即要么相同,要么有 is-a 关系(子类或是实现类)。

<bean id="cb" class="com.why.Student" autowire="byType">
        <property name="name" value="陈八"></property>
        <property name="age" value="20"></property>
    </bean>

如果使用byType方式自动注入要求同源的被调用 bean只能有一个否则会报以下错误
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type ‘com.why.School’ available: expected single matching bean but found 2: mySchool,school

为应用指定多个 Spring 配置文

在实际应用里,随着应用规模的增加,系统中 Bean 数量也大量增加,导致配置文件变得非常庞大、臃肿。为了避免这种情况的产生,提高配置文件的可读性与可维护性,可以将 Spring 配置文件分解成多个配置文件。

spring-school.xml

<bean id="middleSchool" class="com.why.School">
        <property name="schoolName" value="温州中学"></property>
        <property name="addr" value="浙江温州"></property>
    </bean>

spring-student.xml

<bean id="lh" class="com.why.Student">
        <property name="name" value="林浩"></property>
        <property name="age" value="20"></property>
        <!--引用类型-->
        <property name="school" ref="middleSchool"></property>
    </bean>
    <!--引入spring-school.xml-->
    <import resource="classpath:spring-school.xml"/>

applicationContext.xml(主配置文件)

<import resource="classpath:spring-school.xml"/>
    <import resource="classpath:spring-student.xml"/>
    <!--也可使用通配符*-->
    <!-- <import resource="classpath*:spring-*.xml"/> -->

基于注解的 DI

对于 DI 使用注解,将不再需要在 Spring 配置文件中声明 bean 实例。需要在 Spring 配置文件中配置组件扫描器,用于在指定的基本包中扫描注解。

<!--声明组件扫描器:指定注解所在的包-->
    <context:component-scan base-package="com.why"/>
    <!-- 指定多个包
    <context:component-scan base-package="com.why,com.ehy"/> 第1种方式:逗号分隔
    <context:component-scan base-package="com.why;com.ehy"/> 第2种方式:分号分隔
    <context:component-scan base-package="com.why com.ehy"/> 第3种方式:空格分隔
    <context:component-scan base-package="com"/> 第4种方式:指定到父包,也会扫描到子包下级的子包-->

定义bean的注解@Component

需要在类上使用注解@Component,该注解的 value 属性用于指定该bean 的 id 值

//@Component 不指定value属性,bean的id是类名的首字母小写school
@Component("collage")//类似xml中的<bean id="collage" class="com.why.School"></bean>
public class School {
    private String schoolName;
    private String addr;
}
  • @Repository 用于对 DAO 实现类进行注解
  • @Service 用于对 Service 实现类进行注解
  • @Controller 用于对 Controller 实现类进行注解
    这三个注解与@Component 都可以创建对象,但这三个注解还有其他的含义,@Service 创建业务层对象,业务层对象可以加入事务功能,@Controller 注解创建的对象可以作为处理器接收用户的请求。

简单类型属性注入@Value

使用该注解完成属性注入时,类中无需 setter。当然,若属性有 setter,则也可将其加到 setter 上。

@Component("collage")
public class School {
    @Value("浙江大学")
    private String schoolName;
    @Value("浙江杭州")
    private String addr;
    /*toString()*/
}

byType 自动注入@Autowired

@Component("tq")
public class Student {
    @Value("田七")
    private String name;
    @Value("20")
    private int age;
    @Autowired
    private School school;
/*相当于
<bean id="tq" class="com.why.Student" autowire="byType">
	<property name="name" value="田七"></property>
	<property name="age" value="20"></property>
</bean>
*/
}

byName 自动注入@Autowired 与@Qualifier

需要在引用属性上联合使用注解@Autowired 与@Qualifier。@Qualifier的 value 属性用于指定要匹配的 Bean 的 id 值。类中无需 set 方法,也可加到set 方法上。

@Component("tq")
public class Student {
    @Value("田七")
    private String name;
    @Value("20")
    private int age;
    @Autowired
    @Qualifier("middleSchool")
    private School school;

@Autowired 还有一个属性 required,默认值为 true,表示当匹配失败后,会终止程序运行。若将其值设置为 false,则匹配失败,将被忽略,未匹配的属性值为 null。

JDK 注解@Resource 自动注入

Spring 提供了对 jdk 中@Resource 注解的支持。@Resource 注解既可以按名称匹配 Bean,也可以按类型匹配 Bean。默认是按名称注入,采用默认按名称的方式注入按名称不能注入 bean时,则会按照类型进行 Bean 的匹配注入。

@Component("tq")
public class Student {
    @Value("田七")
    private String name;
    @Value("20")
    private int age;
    @Resource //byType
    //@Resouces(name="middleSchool") byName
    private School school;
}

AOP 面向切面编程

AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP 编程术语

  • 切面(Aspect)
    切面泛指交叉业务逻辑。如事务处理、日志处理就可以理解为切面
  • 连接点(JoinPoint)
    连接点指可以被切面织入的具体方法。通常业务接口中的方法均为连接点。
  • 切入点(Pointcut)
    切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。被标记为 final 的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。
  • 目标对象(Target)
    目标对象指将要被增强的对象。即包含主业务逻辑的类的对象。
  • 通知(Advice)
    通知表示切面的执行时间,Advice 也叫增强。通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。切入点定义切入的位置,通知定义切入的时间。

AspectJ

对于 AOP 这种编程思想,很多框架都进行了实现。Spring 就是其中之一,可以完成面向切面编程。然而,AspectJ 也实现了 AOP 的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring 又将AspectJ 的对于 AOP 的实现也引入到了自己的框架中。

AspectJ官网

AspectJ 的通知类型

AspectJ 中常用的通知有五种类型:
(1)前置通知 @Before
(2)后置通知 @AfterReturning
(3)环绕通知 @Around
(4)异常通知 @AfterThrowing
(5)最终通知 @After

AspectJ 的切入点表达式

execution(访问权限类型? 返回值类型 包名类名? 方法名(参数类型和参数个数) 抛出异常类型?)
?表示可选部分
返回值类型和方法名(参数类型和参数数量)是必须要有的
符号意义
*0至多个任意字符
用在方法参数中表示任意多个参数<br>用在包名后面表示当前包以及其子包路径
+用在类名后面表示当前类及子类<br>用在接口后表示当前接口及其实现类

例:

  • execution(public * *(…))
    指定切入点为:任意公共方法。
  • execution(* set*(…))
    指定切入点为:任何一个以“set”开始的方法。
  • execution(* …service..*(…))
    指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点

AspectJ 基于注解的 AOP 实现

1、创建maven工程

2、引入相关依赖
完整依赖

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.why</groupId>
  <artifactId>ch08-Spring-aop</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
  	<!--测试单元-->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <!--spring依赖-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>
    <!--AspectJ依赖-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>5.2.5.RELEASE</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

3、定义业务接口和实现类

public interface SomeService {
    void doSome();
    int doOther(int x,int y);
}

public class SomeServiceImpl implements SomeService {
    @Override
    public void doSome() {
        System.out.println("执行了doSome业务方法");
    }
    @Override
    public int doOther(int x, int y) {
        return x>y ? x : y;
    }
}

4、定义切面类

/**
 *@Aspect:是AspectJ框架的注解表示当前类是切面类
 */
@Aspect
public class MyAspect {

    @Before(value = "execution(* com.why.SomeServiceImpl.doSome(..))")
    public void before(){
        System.out.println("前置通知:在目标方法之前执行,例如输出日志");
    }
}

5、声明目标对象切面类对象

<!--声明目标类对象-->
    <bean id="target" class="com.why.SomeServiceImpl"></bean>

    <!--声明切面类对象-->
    <bean id="myAspect" class="com.why.MyAspect"></bean>

6、注册 AspectJ 的自动代理

<!--声明自动代理生成器,创建代理-->
    <aop:aspectj-autoproxy/>

7、测试

@Test
    public void test01(){
        String resource = "applicationContext.xml";
        ApplicationContext ac = new ClassPathXmlApplicationContext(resource);
        SomeService target = (SomeService) ac.getBean("target");
        target.doSome();
    }

结果

前置通知:在目标方法之前执行,例如输出日志
执行了doSome业务方法

Process finished with exit code 0

JoinPoint 参数

@Before(value = "execution(* com.why.SomeServiceImpl.doOther(..))")
    public void beforeOther(JoinPoint joinPoint){
   	 	/*不光前置通知的方法,可以包含一个 JoinPoint 类型参数,所有的通知方法均可包含该参数。*/
        System.out.println("连接点的方法定义:"+joinPoint.getSignature());
        System.out.println("连接点方法的参数个数:"+joinPoint.getArgs().length);
        /*
            方法参数信息
            Object[] args = joinPoint.getArgs();
        */
    }

调用doOther方法的执行结果

连接点的方法定义:int com.why.SomeService.doOther(int,int)
连接点方法的参数个数:2
执行了other业务方法

后置通知@AfterReturning注解的 returning 属性

在目标方法执行之后执行。由于是目标方法之后执行,所以可以获取到目
标方法的返回值。该注解的 returning 属性就是用于指定接收方法返回值的变
量名的。

@AfterReturning(value = "execution(* com.why.SomeServiceImpl.doOther(..))",returning = "result")
    public void afterReturning(Object result){
        if (result != null) {
            Integer i = (Integer) result;
            result = i * 100;
        }
        System.out.println("后置通知:在目标方法执行后的功能增强,如事务的处理");
        System.out.println("较大数的100倍:"+result);
    }

调用doOther方法后的执行结果

执行了other业务方法
后置通知:在目标方法执行后的功能增强,如事务的处理
较大数的100倍:200

Process finished with exit code 0

环绕通知@Around 方法有ProceedingJoinPoint 参数

在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数。接口 ProceedingJoinPoint 其有一个 proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强
方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。

//接口方法
String doFirst(String name,int age);

//接口方法实现类
@Override
 public String doFirst(String name,int age) {
	System.out.println("执行了doFirst业务方法");
    return "doFirst";
}

定义切面

@Around(value = "execution(* com.why.SomeServiceImpl.doFirst(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        Object object;
        System.out.println("环绕通知:在目标方法之前执行,如输出日志");
        //执行目标方法
        object = pjp.proceed();
        System.out.println("环绕通知:在目标方法之后执行,如事务处理");
        System.out.println(object.toString());
        return object;
    }

异常通知@AfterThrowing注解的 throwing 属性

在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象。当然,被注解为异常通知的方法可以包含一个参数 Throwable,参数名称为 throwing 指定的名称,表示发生的异常对象。

@AfterThrowing(value = "execution(public * com.why.SomeServiceImpl.doSecond(..))",throwing = "ex")
    public void afterThrowing(Throwable ex){
        /*
        异常通知可以做什么?
        把异常发生的时间、地点、原因记录到数据库,日志文件等等
        可以在异常发生时,把异常信息通过邮件、短信发送给开发人员
        */
        System.out.println("异常通知:在目标方法抛出异常执行,异常原因:"+ex.getMessage());
    }
异常通知:在目标方法抛出异常执行,异常原因:/ by zero

java.lang.ArithmeticException: / by zero

Process finished with exit code -1

最终通知@After

无论目标方法是否抛出异常,该增强均会被执行。

@After("execution(public * com.why.SomeServiceImpl.doSecond())")
    public void after(){
        System.out.println("最终通知,总是会执行");
    }

@Pointcut 定义切入点

当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。其用法是,将@Pointcut 注解在一个方法之上,以后所有的 execution 的value 属性值均可使用该方法名作为切入点。代表的就是@Pointcut 定义的切入点。这个使用@Pointcut 注解的方法一般使用 private 的标识方法,即没有实际作用的方法。

@After("myPoint()")
    public void after(){
        System.out.println("最终通知,总是会执行");
    }
	/**
    * @Pointcut:用来定义和管理切面点,简化切入点的定义
    */
    @Pointcut(value = "execution(public * com.why.SomeServiceImpl.doSecond())")
    public void myPoint(){
        //无需写代码
    }

相关文章