快速上手Spring

文章目录

一、Spring概述

1.1 概念

  • Spring是一个项目管理框架,同时也是一套java EE解决方案。
  • Spring是众多优秀设计模式的组合(工厂、单例、代理、适配器、包装器、观察者、模板、策略)。
  • Spring并未替代现有框架产品,而是将众多框架进行有机整合,简化企业级开发,俗称“胶水框架”。

1.2 Spring架构组成

Spring架构由诸多模块组成,可分类为:

  • 核心技术:依赖注入,事件,资源,i18n,验证,数据绑定,类型转换,SpEL,AOP
  • 测试:模拟对象,TestContext框架,SpringMVC测试,WebTestClient。
  • 数据访问:事务,DAO支持,JDBC,ORM,封送XML。
  • Spring MVC和Spring WebFlux Web框架。
  • 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。
  • 语言:Kotlin,Groovy,动态语言。

Spring架构中最核心的一块称为Core Container(核心容器),它搭建并管理着一个“工厂”的设计模式,工厂中用来生产一个一个的对象。Spring有了这样的生产能力之后就可以去管理它所生产的对象,进而把项目中这些对象的协作关系、运行模式加以调整优化,达到对整个项目的一种更优化地管理。

二、自定义工厂

在学习Spring之前我们可以自己搭建一个工厂,来看看工厂的基本工作原理是什么。

2.1 配置文件

bean.properties

userDao=lazydog.dao.UserDaoImpl
userService=lazydog.service.UserServiceImpl

配置文件中的两个类以及其实现类都只有一个deleteUser方法,方法内只有一个打印语句,所以此处省略。

2.2 工厂类

/*
 工厂类
 1.加载配置文件,以确定生产哪些类
 2.生产配置中记录的对应对象
 */
public class MyFactory {
    private Properties properties=new Properties();
    public MyFactory(){}
    public MyFactory(String config) throws IOException {
        //加载配置文件
        properties.load(MyFactory.class.getResourceAsStream(config));
    }
    public Object getBean(String name) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        //通过name获取类路径
        String classPath=properties.getProperty(name);
        //通过反射构建对象
        Class clazz=Class.forName(classPath);
        //返回对象
        return clazz.newInstance();
    }
}

2.3 测试类

public class FactoryTest {
    public static void main(String[] args) throws IOException, IllegalAccessException, InstantiationException, ClassNotFoundException {
        //1.创建工厂对象
        MyFactory myFactory=new MyFactory("/bean.properties");
        //2.利用工厂生产对象
        UserDao userDao = (UserDao) myFactory.getBean("userDao");
        userDao.deleteUser(1);
        UserService userService = (UserService) myFactory.getBean("userService");
        userService.deleteUser(2);
    }
}

运行结果如下,说明生产的类可以正常使用:

delete in Dao.
delete in Service.

三、环境搭建

3.1 构建Maven项目

在Idea中创建Maven项目并在pom中导入Spring相关依赖:

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

3.2 创建Spring配置文件

命名无限制,但约定成俗的命名有:spring-context.xml、applicationContext.xml、beans.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">
    <!-- 需要工厂生产的对象 -->
    <bean id="userDao" class="lazydog.dao.UserDaoImpl"/>
    <bean id="userService" class="lazydog.service.UserServiceImpl"/>
</beans>

3.3 测试一下

在pom中再导入junit包:

<dependency>
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
	<version>4.12</version>
	<scope>test</scope>
</dependency>

创建测试类进行测试:

public class SpringFactory {
    @Test
    public void testSpringFactory(){
        //1.启动工厂
        ApplicationContext context=new ClassPathXmlApplicationContext("/spring-context.xml");
        //2.生产对象
        UserDao userDao = (UserDao) context.getBean("userDao");
        UserService userService = (UserService) context.getBean("userService");
        //3.使用对象
        userDao.deleteUser(1);
        userService.deleteUser(1);
    }
}

运行结果正常。

四、依赖与配置文件

Spring框架包含多个模块,每个模块各司其职,可以结合需求引入相关依赖jar包实现功能。

在3.1节中,导入依赖时只导了一个,但项目实际导入了多个依赖,分别如下图所示:

4.1 依赖关系

箭头的指向表示A依赖需要依赖B依赖,那么导入A依赖时会自动导入B依赖。所以只需要导入最外层jar即可由Maven自动将相关依赖jar引入到项目中。

4.2 schema

schema的意思是规范,3.2节可以看到配置文件中的顶级标签中包含了语义化标签的相关信息。

  • xmlns:语义化标签所在的命名空间。
  • xmlns:xsi:XMLSchema-instance 标签遵循schema标签标准。
  • xsi:schemaLocation:xsd文件位置,用以描述标签语义、属性、取值范围等。

xsd就是xml schema definition,xml规范定义。

五、IoC(控制反转)

Inversion of Control:控制反转。

反转了依赖关系的满足方式,由自己创建的依赖对象,变为由工厂推送(变主动为被动,即为反转)。解决了具有依赖关系的组件之间的强耦合,使得项目形态更加稳健。

所谓依赖关系,比如UserServiceImpl中需要调用Dao层方法,就会去new一个Dao对象;那么service就依赖dao,service满足依赖关系的方式就是new一个dao对象:

public class UserServiceImpl implements UserService{
    //满足依赖关系
    UserDao userDao=new UserDaoImpl();
    @Override
    public void deleteUser(Integer id){
        userDao.deleteUser(1);
        System.out.println("delete in Service.");
    }
}

用new一个对象的方式来满足依赖就会造成强耦合,强耦合是需要极力避免的,否则会有“牵一发而动全身”的麻烦。

我们可以只留一个dao接口,而不去new一个实例对象,然后设置get和set方法:

private UserDao userDao;
@Override
public void deleteUser(Integer id){
    userDao.deleteUser(1);
    System.out.println("delete in Service.");
}
public UserDao getUserDao() {
    return userDao;
}

public void setUserDao(UserDao userDao) {
    this.userDao = userDao;
}

以上代码就没有和任何一个dao类发生耦合,但是同样调用了deleteUser方法,然后修改配置文件:

<bean id="userService" class="lazydog.service.UserServiceImpl">
    <!--为userDao属性赋值,ref值为id为userDao的一个bean-->
    <property name="userDao" ref="userDao"/>
</bean>

然后重新测试一下代码,如果没有发生空指针异常,就说明配置文件生效了;最后运行正常。如果以后需要使用其他dao类,只需要在配置文件中添加响应的bean,然后在属性的ref中改成bean相应的id即可,所以我们并没有修改service本身的代码,这样就会使得代码稳健。这样的一种思想就是IoC。

六、DI(依赖注入)

6.1 概念

在Spring创建对象的同时,为其属性赋值,称之为依赖注入。

在第五节中配置属性的方法就叫做注入,之所以叫依赖注入是因为它是为了完成一种依赖关系,所以控制反转是一种思想,它其实就是一次一次的依赖注入。

6.2 set注入

创建对象时,Spring工厂会通过set方法为对象的属性赋值。

我们创建一个实体类,代码如下(省略了get和set方法):

public class User {
    private Integer id;
    private String password;
    private String sex;
    private Integer age;
    private Date bornDate;
    private String[] hobbies;
    private Set<String> phones;
    private List<String> names;
    private Map<String,String> countries;
    private Properties files;
    //省略了get和set方法
}

然后添加如下配置信息:

<bean id="user" class="lazydog.entity.User">
    <!--简单:包括8中基本类型,String,Date-->
    <property name="id" value="36"/>
    <property name="password" value="123456"/>
    <property name="sex" value="male"/>
    <property name="age" value="22"/>
    <property name="bornDate" value="1999/01/30 12:00:00"/>
    <!--数组-->
    <property name="hobbies">
        <array>
            <value>football</value>
            <value>basketball</value>
        </array>
    </property>
    <!--集合-->
    <property name="names">
        <list>
            <value>tang</value>
            <value>wang</value>
        </list>
    </property>
    <property name="phones">
        <set>
            <value>15299999999</value>
            <value>13566666666</value>
        </set>
    </property>
    <property name="countries">
        <map>
            <entry key="zh" value="china"/>
            <entry key="en" value="english"/>
        </map>
    </property>
    <property name="files">
        <props>
            <prop key="username">root</prop>
            <prop key="url">jdbc:mysql:xxx</prop>
        </props>
    </property>
</bean>

接下来编写一个测试类,来查看是否复制成功:

@Test
public void testSet(){
    ApplicationContext context=new ClassPathXmlApplicationContext("/spring-context.xml");
    User user = (User) context.getBean("user");
    System.out.println("ss");//此处设断点,然后debug
}

我们可以在调试界面看到user的属性已经成功通过set注入赋值了。

User类可能存在jdk之外的类型比如Address,称为自建类型,也可以通过set注入的方式赋值,只需要多写一个Address类的bean标签,然后设置属性值(方式同上),然后在user的bean标签中添加一个属性名为address,ref值为Address的bean的id即可。

七、Bean细节

7.1 控制简单对象的单、多例模式

在上几节的测试类中由工厂生产的对象都是同一个(哪怕调用多次getBean方法),这种叫做单例模式。

通过配置可以设置为单/多例模式。

<!--
	singleton(默认):每次调用工厂,得到的都是同一个对象;
	prototype:每次调用工厂,都会创建新的对象。
-->
  • 需要根据场景决定对象的单例、多例模式。
  • 可以共用:Service、Dao、SqlSessionFactory(或者是所有的工厂)。
  • 不可共用:Connection、SqlSession、ShoppingCart。

八、Spring工厂特性

8.1 饿汉式创建优势

工厂创建之后,会将Spring配置文件中所有对象都创建完成(饿汉式)。

它可以提高程序运行效率,避免多次IO,减少对象创建时间(概念接近连接池,一次性创建好,使用时直接获取)。

8.2 生命周期方法

  • 自定义初始化方法:添加“init-method”属性,Spring则会在创建对象之后,调用此方法。
  • 自定义销毁方法:添加“destory-method”属性,Spring则会在销毁对象之前,调用此方法。
  • 销毁:工厂的close方法被调用之后,Spring会销毁所有已创建的单例对象。
  • 分类:Singleton对象由Spring容器销毁、Prototype对象由JVM销毁。

我们在使用工厂创建一个单例对象时,可以自己写一个初始化和销毁方法来体验单例对象的生命周期:

public class User{
    //属性及其他代码省略
    public User() {
        System.out.println("构造方法执行。");
    }
    //初始化方法
    public void initUser(){
        System.out.println("初始化。");
    }
    //销毁方法
    public void destoryUser(){
        System.out.println("销毁。");
    }
}

在配置文件中添加属性来使自定义方法生效:

<bean id="user" class="lazydog.entity.User" init-method="initUser" destroy-method="destoryUser" />

然后在测试类中进行测试:

@Test
public void testLife(){
    ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("/spring-context.xml");
    User user = (User) context.getBean("user");
    System.out.println("==========");
    context.close();
}

运行结果如下:

构造方法执行。
初始化。
==========
销毁。

以上是单例对象的生命周期,多例对象略有不同。

8.3 生命周期阶段

单例bean: singleton
随工厂启动创建--》构造方法--》set方法(注入值)--》init(初始化)--》构建完成--》随工厂关闭 销毁
多例bean: prototype
被使用时创建--》构造方法--》set方法(注入值)--》init(初始化)--》构建完成--》JVM垃圾回收 销毁

九、代理设计模式

9.1 概念

将核心功能与辅助功能(事务、日志、性能监控代码)分离,达到核心业务更纯粹,辅助功能业务可复用。

9.2 静态代理设计模式

通过代理类的对象,为原始类的对象(目标类的对象)添加辅助功能,更容易更换代理实现类,利于维护。

比如出租一个房子你需要做四件事,但作为房东你只关心两件事,这两件事就是核心功能:

public class landlordServiceImpl implements landlordService{
    @Override
    public void rent() {
        //辅助功能/额外功能
        System.out.println("发布租房信息。");
        System.out.println("带租客看房。");
        
        //核心功能
        System.out.println("签合同。");
        System.out.println("收房租。");
    }
}

我们可以写一个代理类,来将这个原始类的辅助功能处理掉:

public class landlordProxy implements landlordService{
    landlordService landlordService=new landlordServiceImpl();
    @Override
    public void rent() {
        //辅助功能/额外功能
        System.out.println("发布租房信息。");
        System.out.println("带租客看房。");
        
        landlordService.rent();
    }
}

这样原始类就可以专心关注核心功能,而作为租客,只需要调用这个代理类就可以了。

这种由我们明确写出来的代理类叫做静态代理类

9.3 动态代理设计模式

动态创建代理类的对象,为原始类的对象添加辅助功能。

9.3.1 JDK动态代理实现(基于接口)

@Test
public void testJDK(){
    //目标
    landlordService ls=new landlordServiceImpl();
    //辅助功能,额外功能
    InvocationHandler invocationHandler=new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("发布租房信息。");
            System.out.println("带租客看房。");
            ls.rent();
            return null;
        }
    };
    //动态生成代理类
    landlordService proxy = (landlordService) Proxy.newProxyInstance(DynamicProxy.class.getClassLoader(),
                                                                     ls.getClass().getInterfaces(),
                                                                     invocationHandler);
    proxy.rent();
}

proxy这个对象是动态生成出来的,我们看不到它是哪一个类,这就是所谓的动态代理

9.3.2 CGlib动态代理实现(基于继承)

@Test
public void testCGlib(){
    //目标
    landlordService ls=new landlordServiceImpl();
    Enhancer enhancer = new Enhancer();
    //代理类需要和目标类功能一致。所以它除了实现相同的接口外(jdk),还可以把代理类作为父类
    enhancer.setSuperclass(landlordServiceImpl.class);
    //辅助功能
    enhancer.setCallback(new org.springframework.cglib.proxy.InvocationHandler() {
        @Override
        public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
            System.out.println("发布租房信息。");
            System.out.println("带租客看房。");
            ls.rent();
            return null;
        }
    });
    //动态生成代理类
    landlordServiceImpl proxy = (landlordServiceImpl) enhancer.create();
    proxy.rent();
}

这两种动态代理方法看上去是很抽象的,实际上也不会去这么去使用,这两种代理方法会在AOP中被封装到底层,所以我们真正使用动态代理的时候是会借助AOP的支持的。

十、面向切面编程

10.1 概念及术语

AOP(Aspect Oriented Programming),即面向切面编程,利用一种称为“横切”的技术,剖开封装的内部对象,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为“Aspect”,即切面。所谓”切面“,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

Spring的AOP编程即是通过动态代理类为原始类的方法添加辅助功能

  • 连接点(Joinpoint):连接点是程序类中客观存在的方法,可被Spring拦截并切入内容。
  • 切入点(Pointcut):被Spring切入连接点。
  • 通知增强(Advice):可以为切入点添加额外功能,分为前置通知、后置通知、异常通知、环绕通知等。
  • 目标对象(Target):代理的目标对象。
  • 引介(Introduction):一种特殊的增强,可在运行期为类动态添加Field和Method。
  • 织入(Weaving) :把通知应用到具体的类,进而创建的代理类的过程。
  • 代理(Proxy):被AOP织入通知后,产生的结果类。
  • 切面(Aspect):由切点和通知组成,将横切逻辑织入切面所指定的连接点中。

10.2 开发流程

以前文的租房事件为案例,简单展示面向切面的开发流程。

10.2.1 环境搭建

引入AOP相关依赖:

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>

10.2.2 编写增强方法

同9.2节代码,将rent的额外功能提取出来,使rent方法保持单一职能,便于维护。

public class landlordBeforeAdvice implements MethodBeforeAdvice {
    //额外功能
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("发布租房信息。");
        System.out.println("带租客看房。");
    }
}

通知分为前后置通知等,因为rent的额外功能需要在核心功能前面出现,所以此处为前置通知,增强方法需要实现MethodBeforeAdvice接口,并实现before方法。

10.2.3 配置文件

新建spring配置文件,文件名通常为spring-context.xml,文件内容如下:

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--目标对象:原始业务-->
    <bean id="landlordService" class="lazydog.service.landlordServiceImpl"/>
    <!--通知:额外功能-->
    <bean id="landlordBeforeAdvice" class="lazydog.advice.landlordBeforeAdvice"/>
    <!--编织-->
    <aop:config>
        <!--切入点-->
        <aop:pointcut id="landlord_rent" expression="execution(public void rent())"/>
        <!--组装-->
        <aop:advisor advice-ref="landlordBeforeAdvice" pointcut-ref="landlord_rent"/>
    </aop:config>
</beans>

同3.2节的配置文件比较,多了一个命名空间xmlns="http://www.springframework.org/schema/aop",因为命名空间不能重复,所以通常取名为aop。

config即为aop命名空间中的标签,其中定义切入点,也就是告诉spring谁是需要切入的方法(需要追加额外功能的方法)以及额外功能是什么,这一整个过程称为编织。

pointcut标签定义切入点,expression填写被增强的方法,需要使用execution()切入点表达式,括号中填写完整的方法定义。此处为rent方法,其中只包括租房的核心功能。

advisor标签有两个属性,advice-ref用来告诉spring谁是增强方法(额外功能),其值为增强方法bean的id值;pointcut-ref用来告诉spring把这个增强方法(额外功能)切入给谁,其值为对应pointcut标签的的id值。

将配置文件完成后,开发流程就大致结束了。

10.2.4 测试代码

@Test
public void testSpringAop(){
    //启动工厂
    ApplicationContext ApplicationContext = new ClassPathXmlApplicationContext("/spring-context.xml");
    //通过目标bean的id获得其代理类
    landlordService landlordService = (lazydog.service.landlordService) ApplicationContext.getBean("landlordService");
    landlordService.rent();
}

我们并没有创建代理类,也没有在配置文件中定义代理类的bean,但我们需要使用代理类来调用rent方法,是否有遗漏的地方?

其实在调用getBean()方法时,spring返回的就是目标类的代理类,方法参数写上目标类bean的id就可以了。

结果如下:

发布租房信息。
带租客看房。
签合同。
收房租。

这样,在rent方法中只有后两句核心功能,将代码耦合度降至了最低。

10.3 通知类

10.2.2节就是一个通知类,除了前置通知,还有以下几种通知:

  • 后置通知:AfterAdvice
  • 后置通知:AfterReturningAdvice
  • 异常通知:ThrowsAdvice
  • 环绕通知:MethodIntercptor

AfterAdvice是AfterReturningAdvice和ThrowsAdvice的父类。

AfterAdvice在核心功能之后执行,不论核心是否产生异常。

AfterReturningAdvice在核心功能之后执行,如果有异常则不执行,方法因异常而结束,无返回值。

ThrowsAdvice在核心功能之后执行,但只有产生异常时才执行。

MethodIntercptor在核心功能前后都执行。

本文只演示最后一种通知。

public class landlordAroundAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("开始!");
        Object proceed = methodInvocation.proceed();
        System.out.println("结束!");
        return proceed;
    }
}

methodInvocation.proceed()方法触发核心功能的执行,在这前后分别编写环绕的功能。

在配置文件中写入通知类bean后组装:

<bean id="landlordAroundAdvice" class="lazydog.advice.landlordAroundAdvice"/>
<aop:config>
    <!--切入点-->
    <aop:pointcut id="landlord_rent" expression="execution(public void rent())"/>
    <!--组装-->
    <aop:advisor advice-ref="landlordAroundAdvice" pointcut-ref="landlord_rent"/>
</aop:config>

测试结果如下:

开始!
签合同。
收房租。
结束!

10.4 通配切入点

execution()切入点表达式中按照“访问修饰符(可省略)+返回值+包名.类+方法名(参数列表)”填写,但也有很多便捷的通配写法

  • 匹配参数:<aop:pointcut id=“myPointCut” expression=“execution(* *(lazydog.entity.User))” />。

    该表达式忽略返回值和方法名,只要是User类的参数就切入。

  • 匹配方法名(无参):<aop:pointcut id=“myPointCut” expression=“execution(* rent())” />

    该表达式忽略返回值,只要方法名为rent且无参就切入。

  • 匹配方法名(任意参数):<aop:pointcut id=“myPointCut” expression=“execution(* rent(…))” />

    该表达式忽略返回值和参数列表,只要方法名为rent就切入。

  • 匹配返回值类型:<aop:pointcut id=“myPointCut” expression=“execution(lazydog.entity.User *(…))” />

    该表达式忽略方法名和参数列表,只要返回值为User就切入。

  • 匹配类名:<aop:pointcut id=“myPointCut” expression=“execution(* lazydog.entity.UserServiceImpl.*(…))” />

    该表达式忽略返回值、方法名和参数列表,只要方法的类名为UserServiceImpl就切入。

  • 匹配包名:<aop:pointcut id=“myPointCut” expression=“execution(* lazydog.service..(…))” />

    该表达式忽略返回值、方法名和参数列表,只要是lazydog.service包下的方法就切入。

  • 匹配包名以及子包名:<aop:pointcut id=“myPointCut” expression=“execution(* lazydog.service….(…))” />

    该表达式忽略返回值、方法名和参数列表,只要是lazydog.service包及其子包下的方法就切入。

10.5 Spring代理实现的选择

Spring底层包含了JDK动态代理和CGlib动态代理两种机制,它是如何选择这两种机制的呢?

它的核心选择发生在DefaultAopProxyFactory类中,在idea中双击shift查找该类查看源代码:

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    if (!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {
        return new JdkDynamicAopProxy(config);
    } else {
        Class<?> targetClass = config.getTargetClass();
        if (targetClass == null) {
            throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");
        } else {
            return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));
        }
    }
}

该方法中的if-else语句就是在做代理机制的选择。

第一个if有三个判断条件,config.isOptimize()和config.isProxyTargetClass()默认返回的false。前者通过cglib控制的代理使用优化策略;后者设置直接通过目标类来代理,而不是接口,可在配置文件中config标签里进行设置,为true就使用cglib代理机制:

<aop:config proxy-target-class="true">
</aop:config>

前两者默认为false,所以由this.hasNoUserSuppliedProxyInterfaces(config)决定代理机制。它的意思是目标(代理对象)是否实现了接口,有实现接口的话就return new JdkDynamicAopProxy(config);使用jdk代理机制。

最后一个else分支的返回语句使用了三目表达式,targetClass.isInterface的意思是代理对象是否是Interface类型,是的话仍然使用jdk代理;Proxy.isProxyClass(targetClass)的意思是当代理目标是getProxyClass方法或者newProxyInstance方法生成时,仍然使用jdk代理。

简而言之的基本规则为:

目标业务类如果有接口使用JDK代理否则使用CDlib代理方式

10.6 后处理器

Spring中定义了很多后处理器。每个bean在创建完成之前,都会有一个后处理器过程,即再加工,对bean做出相关改变和调整。

spring-AOP中就有一个专门的后处理器,负责通过原始业务组件(Service),再加工得到一个代理组件。

8.3节中展示了bean的生命周期,那么后处理器是在生命周期的哪个阶段呢?

我们自己定义一个后处理器,通过打印语句来体验bean的生命周期。

10.6.1 定义后处理器

首先定义一个实体User类供测试:

public class User {
    public Integer id;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        System.out.println("Set User");
        this.id = id;
    }

    public User(Integer id) {
        this.id = id;
    }

    public User() {
        System.out.println("构造User");
    }
    public void initUser(){
        System.out.println("init User");
    }
}

编写后处理器类:

//后处理器
//bean生命周期:构造、set、init、destroy
public class MyBeanPostProcessor implements BeanPostProcessor {
    //参数bean是构造和set结束后的原始bean对象,beanName为bean对应的id
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("后处理1");
        System.out.println(bean+" :"+beanName);
        return bean;
    }

    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("后处理2");
        System.out.println(bean+" :"+beanName);
        return bean;
    }
}

自己创建的后处理器需要实现BeanPostProcessor接口,编译器不会提示你需要实现方法,可以通过按住ctrl同时点击接口名查看源代码,将这两个方法复制到自己的方法中。

10.6.2 配置后处理器

<bean class="lazydog.entity.User" init-method="initUser">
    <property name="id" value="1"/>
</bean>
<!--后处理器-->
<bean class="lazydog.processor.MyBeanPostProcessor"/>

通过注入一个id值来使bean展示生命阶段。

10.6.3 测试后处理器

@Test
public void testSpringAop(){
    //启动工厂
    ApplicationContext ApplicationContext = new ClassPathXmlApplicationContext("/spring-context.xml");
}

只需要启动一下工厂就可以了,bean随工厂启动而创建。

结果如下:

构造User
Set User
后处理1
lazydog.entity.User@77ec78b9 :lazydog.entity.User#0
init User
后处理2
lazydog.entity.User@77ec78b9 :lazydog.entity.User#0

10.6.4 Bean生命周期

实际工作中不用自己写后处理器,此处为了体验bean的生命阶段,由上述例子,可以看出bean的生命周期是:

构造--》注入属性--》后处理器前置过程--》初始化--》后处理器后置过程--》返回--》销毁

动态代理的创建时刻也是在后处理器过程中完成的。

十一、整合Mybatis

11.1 配置数据源

新建maven项目,创建配置文件ApplicationContext.xml,将数据源配置到项目中。

11.1.1 引入properties配置文件

#jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/userdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
jdbc.username=root
jdbc.password=170912036
#初始化连接
initialSize=10
#最大连接数量
maxActive=50
#最小空闲连接
minIdle=5
#超时等待时间,以毫秒为单位
maxWait=10000
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis=60000
#配置一个连接在池中最小生存连接,单位是毫秒
minEvictableIdleTimeMillis=60000

11.1.2 整合Spring配置

<!--ApplicationContext.xml-->
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--使用了Druid连接池-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="initialSize" value="${initialSize}"/>
        <property name="maxActive" value="${maxActive}"/>
        <property name="minIdle" value="${minIdle}"/>
        <property name="maxWait" value="${maxWait}"/>
        <property name="timeBetweenEvictionRunsMillis" value="${timeBetweenEvictionRunsMillis}"/>
        <property name="minEvictableIdleTimeMillis" value="${minEvictableIdleTimeMillis}"/>
    </bean>
</beans>

11.1.3 导入相关依赖

<!--Spring相关依赖-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.3.5</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.1.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.6.RELEASE</version>
</dependency>
<!--Mybatis-->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.6</version>
</dependency>
<!--Mysql驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.21</version>
</dependency>
<!--druid连接池-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.5</version>
</dependency>

11.2 整合Mybatis

11.2.1 导入项目依赖

<!--Spring整合Mybatis-->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.6</version>
</dependency>

11.2.2 配置SqlSessionFactory

<!--ApplicationContext.xml-->
<!--生产SqlSessionFactory-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <!--注入连接池-->
    <property name="dataSource" ref="dataSource"/>
    <!--注入dao-mapper文件信息,如果映射文件和dao接口同包且同名,则此配置可省略-->
    <property name="mapperLocations">
        <list>
            <value>classpath:lazydog/dao/*.xml</value>
        </list>
    </property>
    <!--为dao-mapper文件中的实体定义省缺包路径-->
    <property name="typeAliasesPackage" value="lazydog.entity"/>
</bean>

mapperLocations属性指定映射文件存放目录;typeAliasesPackage属性指定别名的同名策略,别名与实体类名同名。

11.2.3 整合测试

测试需要一个数据库表,表名为t_user,其中插入几条数据,其对应的实体类如下:

public class User {
    public Integer id;
    public String username;
    public String password;
    public Character gender;
    public Date register_time;
    //get和set方法、构造方法等省略
}

创建DAO接口及其映射文件:

public interface UserDao {
    List<User> queryAllUsers();
}
<!--UserDaoMapper.xml-->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mytais.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="lazydog.dao.UserDao">
    <select id="queryAllUser" resultType="User">
        select id,username,password,gender,register_time
        from t_user
    </select>
</mapper>

导入junit测试依赖:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

编写测试代码:

public class testSpringMybatis {
    @Test
    public void test01(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/ApplicationContext.xml");
        SqlSessionFactory sqlSessionFactory = (SqlSessionFactory) applicationContext.getBean("sqlSessionFactory");
        SqlSession sqlSession = sqlSessionFactory.openSession();
        List<User> users=new ArrayList<>();
        UserDao mapper = sqlSession.getMapper(UserDao.class);
        List<User> allUser = mapper.queryAllUser();
        for (User user:allUser
            ) {
            System.out.println(user);
        }
    }
}

首先加载spring配置文件启动工厂,bean随工厂启动而创建,利用工厂获取sqlSessionFactory对象,然后得到sqlSession对象,之后就和Mybatis操作一样,获得mapper对象,调用方法,结果执行正常。

11.2.4 配置MapperScannerConfigurer

<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <!--dao接口所在的包,如果有多个包用逗号或分号隔开-->
    <property name="basePackage" value="lazydog.dao"/>
    <!--如果工厂中只有一个SqlSessionFactory的bean,此配置可省略-->
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

MapperScannerConfigurer管理DAO实现类的创建,它扫描所有DAO接口并构建DAO实现,创建DAO对象存入工厂管理。

DAO实现类的对象在工厂中的id是“首字母小写+接口类的类名”,如UserDao=》userDao;OrderDao=》orderDao。

编写测试代码:

@Test
public void testDao(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/ApplicationContext.xml");
    UserDao userDao = (UserDao) applicationContext.getBean("userDao");
    List<User> users = userDao.queryAllUser();
    for (User user:users
        ) {
        System.out.println(user);
    }
}

ApplicationContext.xml并没有定义id为userDao的bean,却成功拿到了bean对象,说明配置成功。

11.2.5 配置Service

首先创建service接口及其实现类:

public interface UserService {
    List<User> queryAllUsers();
}
public class UserServiceImpl implements UserService {
    UserDao userDao;

    public UserDao getUserDao() {
        return userDao;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public List<User> queryAllUsers() {
        return userDao.queryAllUsers();
    }
}

配置文件:

<bean id="userService" class="lazydog.service.UserServiceImpl">
    <property name="userDao" ref="userDao"/>
</bean>

ref的值为 id为userDao的bean,这个bean是不是我们写的,而是上一节中MapperScannerConfigurer配置所创建的。

测试代码:

@Test
public void testService(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/ApplicationContext.xml");
    UserService userService = (UserService) applicationContext.getBean("userService");
    List<User> users = userService.queryAllUser();
    for (User user:users
        ) {
        System.out.println(user);
    }
}

运行成功。

十二、事务

12.1 配置事务管理器

DataSourceTransactionManager是一个事务管理器组件,它帮我们管理事务管理的相关功能,比如提交和回滚。

<bean id="tx" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

DataSourceTransactionManager和SqlSessionFactoryBean需要注入同一个dataSource的bean,否则事务控制失败。

12.2 配置事务通知

它基于事务管理器,进一步定制,生成一个额外功能(Advice)。

事务需要在核心之前开启,核心之后提交,据此我们可以写一个环绕通知来完成事务控制;但实际上Spring帮我们完成了大部分工作,我们只需要做一些配置就可以拥有控制事务的额外功能。

<tx:advice id="txManager" transaction-manager="tx">
    <!--定义事务属性-->
    <tx:attributes>
        <!--对于以User结尾的方法,切入时实行对应事务-->
        <tx:method name="*User" rollback-for="Exception"/>
        <!--对于以query开头的方法,切入时实行对应事务-->
        <tx:method name="query*" propagation="SUPPORTS"/>
        <!--剩余所有方法-->
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

注意advice标签使用了tx别名,在文件头加入以下内容:

<beans <!--加入如下内容-->
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/tx
                           http://www.springframework.org/schema/tx/spring-tx.xsd">
</beans>

advice标签的id属性填写一个标识即可(任意);transaction-manager指定事务管理器,也就是12.1节的bean的id;该标签基于事务管理器的逻辑定制出一个事务通知。

method标签的name属性添加被切入的方法名,除此之外还能定制其他的事务属性,下面会对事务属性进行详细介绍。

12.3 事务属性

12.3.1 隔离级别

事务之间是有隔离的,隔离级别有高有低,它们主要影响着并发性安全性两个方面。

安全性:级别越高,多事务并发时,越安全;因为共享的数据越来越少,事务间彼此干扰减少。

并发性:级别越高,多事务并发时,并发越差;因为共享的数据越来越少,事务间阻塞情况增多。

method标签的属性isolation隔离级别,它有如下的取值:

  • DEFAULT:默认值,采用数据库默认的设置(建议);
  • READ-UNCOMMITED:读未提交;
  • READ-COMMITED:读提交;(Oracle数据库默认隔离级别)
  • REPEATABLE-READ:可重复读;(MySql数据库默认隔离级别)
  • SERIALIZED-READ:序列化读。

隔离级别从低到高为:

READ-UNCOMMITED < READ-COMMITED < REPEATABLE-READ < SERIALIZED-READ

多事务并发时的安全问题:

  • 脏读:一个事务读取到另一个事务还未提交的数据。隔离级别大于等于READ-COMMITED可防止。
  • 不可重复读:一个事务内多次读取一行数据的相同内容,其结果不一致。隔离级别大于等于REPEATABLE-READ可防止。
  • 幻影读:个事务内多次读取一张表中的相同内容,其结果(表行数)不一致。隔离级别大于等于SERIALIZED-READ可防止。

12.3.2 传播行为

method标签的属性propagation传播行为

先来解释以下什么叫事务嵌套。假设有两个Service,它们分别有各自的方法,方法内有自己的事务控制;当Service1调用了Service2的方法时,就会发生事务嵌套,当Service2的方法发生回滚时,可能只会影响Service2的事务控制,而不影响Service1的事务控制。

当涉及到事务嵌套时,可能会存在问题。属性propagation有以下取值:

  • SUPPORTS:不存在外部事务,则不开启新事务;存在外部事物,则合并到外部事务中,也就是说自己发生错误会回滚,外部发生错误也会跟它回滚。(适合查询)
  • REQUIRED:不存在外部事务,则开启新事务;存在外部事物,则合并到外部事务中。(默认值,适合增删改)

据此,我们可以再配置一下事务通知:

<tx:method name="queryAllUser" isolation="DEFAULT" propagation="SUPPORTS" rollback-for="Exception"/>
<tx:method name="insertUser" propagation="REQUIRED"/>

12.3.3 读写性

method标签的属性read-only读写性,它有以下取值:

  • true:只读,可以提高查询效率。
  • false:默认值,可读可写,适合增删改。

据此,我们可以再配置一下事务通知:

<tx:method name="queryAllUser" read-only="true" propagation="SUPPORTS" rollback-for="Exception"/>
<tx:method name="insertUser" />

12.3.4 事务超时

method标签的属性timeout事务超时时间(单位秒)。

事务在执行过程中拿不到需要的资源就会被阻塞,进入等待状态,如果一直等待有可能变成出现死锁,所以需要给事务设置一个等待时间,超时后就会报错并停止事务。

该属性取值为**-1**时,由数据库指定等待时间,这也是默认值(建议设置)。

12.3.5 事务回滚

method标签的属性rollback-for事务回滚

默认如果事务中抛出RuntimeException,则自动回滚;

如果事务中抛出CheckException(非运行时异常),不会自动回滚,而是默认提交事务。

可以通过将该属性值设为“Exception”来进行回滚,或者将CheckException转换成RuntimeException上抛。

据此,我们可以再配置一下事务通知:

<tx:method name="queryAllUser" read-only="true" propagation="SUPPORTS" rollback-for="Exception"/>
<tx:method name="insertUser" rollback-for="Exception" />

12.3.6 编织

最后的事务通知定义:

<tx:advice id="txManager" transaction-manager="tx">
    <!--定义事务属性-->
    <tx:attributes>
        <tx:method name="queryAllUsers" propagation="SUPPORTS" rollback-for="Exception"/>
        <tx:method name="*User" rollback-for="Exception"/>
        <!--剩余所有方法-->
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

定义完事务通知后,要将事务管理的Advice切入需要事务的业务方法中。

<aop:config>
    <aop:pointcut id="tx_userService" expression="execution(* lazydog.service.UserServiceImpl.*(..))"/>
    <aop:advisor advice-ref="txManager" pointcut-ref="tx_userService"/>
</aop:config>

测试代码:

@Test
public void testTx(){
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/ApplicationContext.xml");
    UserService userService = (UserService) applicationContext.getBean("userService");
    userService.deleteUser(2);
}

查看数据库已成功删除。

这种控制事务的方法叫做声明式控制。

十三、注解开发

使用注解可以简化我们的开发步骤,很多注解都可以代替代码配置。

13.1 声明Bean

这类注解可以替换自建类型组件的标签,更快速的声明bean。

声明Bean的注解有如下几类:

  • @Service:业务类专用;
  • @Repository:dao实现类专业;
  • @Controller:web层专用;
  • @Component:通用;
  • @Scope:用户控制bean的创建模式。

前四个注解除了名称不一样其他基本一样。

@Service("userService")
public class UserServiceImpl implements UserService {
    //代码略
}

在类上标注一个注解,这样写就相当于在配置文件中的:

<bean id="userService" class="lazydog.service.UserServiceImpl"/>

也可以单写注解,不加括号,id名即为首字母小写的类名,即userServiceImpl。

前四种用法一致,不再一一演示。

Spring默认是单例模式,如果你需要多例模式就可以使用Scope注解:

@Service("userService")
@Scope("prototype")
public class UserServiceImpl implements UserService {
    //代码略
}

13.2 注入

注入注解用于完成bean属性的注入。

注入注解有以下几类:

  • @Autowired:基于类型自动注入;
  • @Resource:基于名称自动注入;
  • @Qualifier(“userDao”):限定注入的bean的id,一般与@Autowired联用。
  • @Value:注入简单类型数据(jdk8中基本类型+String)
@Service("userService")
public class UserServiceImpl implements UserService {
    @Resource(name = "userDao")
    private UserDao userDao;
}

@Resource括号内的name属性指定了注入的bean的id值为userDao;括号可以省略,那么它就会寻找与属性同名的bean id进行注入。

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

@Autowired会寻找类型为UserDao类型的bean进行注入,当配置文件中有多个同类型的bean时可能会出现问题,那么可以使用@Qualifier(“userDao”)来指定id值。

@Value("10")
private Integer testId;

@Value的使用很简单,在属性上打赏标注,括号内填值即可。

13.3 事务控制

这类注解用于控制事务切入,专用于业务类,因为业务类才需要做事务控制。

  • @Transactional:可以加在类上或者方法上,加在类上表示应用于类种所有方法。
@Transactional(isolation = Isolation.DEFAULT,propagation = Propagation.REQUIRED,timeout = -1,readOnly = false,rollbackFor = Exception.class)
public class UserServiceImpl implements UserService {
}

同配置文件,我们也可以在括号种设置事务属性。

当给方法单独添加事务注解时,类上的事务注解就会失效。

13.4 注解所需配置

进行注解开发后,注解可能存在在各种类和方法上,我们需要告诉Spring这些注解位置,然后进行识别和解析。

<!--告诉Spring注解位置,填写一个父包,Spring就会寻找包下类的注解-->
<context:component-scan base-package="lazydog"/>
<!--单独配置Transactional注解,指定事务管理器-->
<tx:annotation-driven transaction-manager="tx"/>

进行完如上的设置后,12.6.6节的配置就可以全部注释掉了。

我们可以自行使用注解进行配置,繁琐操作不再演示。

13.5 AOP开发

//声明此类是一个切面类,包含切入点(pointcut)和通知(Advice)
@Aspect
//声明bean,进入工厂
@Component
public class MyAspect {
    //定义切入点,pc()方法只是一种形式
    @Pointcut("execution(* lazydog.service.UserServiceImpl.*())")
    public void pc(){}

    //前置通知
    //引用pc()方法,本质是在引入切入点
    //参数JoinPoint是连接点类
    @Before("pc()")
    public void MyBefore(JoinPoint a){
        //该方法获得当前目标
        System.out.println("target : "+a.getTarget());
        //该方法获得当前方法的参数表
        System.out.println("args : "+a.getArgs());
        //该方法获得当前调用方法的名称
        System.out.println("method's name : "+a.getSignature().getName());
        //以上都是可选方法
        System.out.println("before~~~");
    }

    //后置通知
    //value引用切入点;returning获得核心功能的返回值,同方法参数的ret
    @AfterReturning(value = "pc()",returning = "ret")
    public void MyAfterReturning(JoinPoint a,Object ret){
        System.out.println("after~~~ : "+ret);
    }

    //环绕通知
    //proceed()方法执行核心功能,proceed对象存放核心功能返回的代码
    @Around("pc()")
    public Object MyInterceptor(ProceedingJoinPoint p) throws Throwable {
        System.out.println("interceptor~~~1");
        Object proceed = p.proceed();
        System.out.println("interceptor~~~2");
        return proceed;
    }

    //异常通知
    //ex用来保存当前的异常,对应参数中的e
    @AfterThrowing(value = "pc()",throwing = "ex")
    public void MyThrows(JoinPoint a,Exception e){
        System.out.println("throws : "+e.getMessage());
    }
}

代码区块中所展示的6个标签分别为:

  • @Aspect:声明切面类;
  • @Pointcut:定义切入点;
  • @Before:前置通知;
  • @AfterReturning:后置通知;
  • @Around:环绕通知;
  • @AfterThrowing:异常通知。

但仅仅是添加注解是没用的,还需要再配置中开启:

<!--启用aop注解-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

当然上述配置是在13.4配置的基础上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值