spring是一个很大的生态圈,里面有很多技术。
其中最基础的是spring framework,主要的技术 是springboot以及springcloud。
spring framework是spring生态圈中最基础的项目,是其他项目的基础。
IoC控制反转:使用对象时,在程序方法中不要主动使用new创建对象,而是由外部提供对象,对象的控制权由程序转移到外部,这种思想叫做控制反转。这种思想的作用就是为了解耦。spring对IoC思想做了实现。spring提供了一个容器,称为IoC容器,用来充当IoC思想中的外部。
IoC管理的是service和dao之中的对象。
IoC容器负责对象的创建、初始化等一系列化工作,被创建和被管理的对象在IoC容器中被统称为bean。
有时候,bean与bean之间有互相依赖的关系(比如service层的bean。肯定是要跟dao层的bean联系到一起的,service层的bean要创建一个dao层的bean),这时候,IoC容器会自动产生依赖。这个过程叫做DI(依赖注入)。
IoC容器主要用于解耦。
接口就是一个空壳,里面只写了规范,实现类才是接口的灵魂。
首先,导入springframework的坐标
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
? 导入依赖完毕后,idea已经识别到了坐标的导入,idea会在new file那里显示可以创建spring的配置文件。在resource之中创建一个就行了。把文件名取为applicationContext。接着,在applicateionContext里边配置bean。
? 接下来就可以创建IoC容器,直接使用IoC容器来获取bean了(项目的准备工作已经创建了一个BookDao接口以及它的实现类)。IoC容器创建bean,实际上也是通过调用实现类的无参构造方法(下面会讲到)。
注意,bean的id不能重复。
由于BookService之中存在BookDao,在创建BookService的bean的时候需要创建BookDao的bean,所以需要在配置文件中的BookService之中使用属性表明BookService依赖于BookDao。
bean标签中,除了id和class以外,还可以再配置一个属性scope
。默认是singleton(单例化),可以置为prototype实现非单例化。所谓单例和非单例,意思就是IoC容器是否会重复创建bean对象。假设有一个service层的接口EmpService,如果是单例,IoC容器只会在第一次用的时候创建出这个对象,之后一直保存这个对象(其实无影响,因为接口中的方法执行完毕后不会改变对象的字段),下一次用就从容器中取出该对象即可;如果是非单例对象,每一次用,IoC容器都会创建一个新的EmpService对象。
适合交给IoC管理的bean:单例对象(controller层、service层、dao层、工具对象);
不适合交给IoC管理的bean:非单例对象(entity/pojo中的实体)
bean是如何被创建出来的?spring程序启动时,
bean标签里id写给这个bean起的名字,class写bean所在的位置。当使用IoC获取bean对象时,IoC会直接使用无参构造方法构造一个对象。构造器实例化对象全部都是通过空参构造器进行构造的。一旦不存在无参构造方法,spring将抛出异常。
配置文件中写<bean>
,会让spring自动使用无参构造方法对bean进行实例化。
创建一个工厂类,在工厂类的方法中new出一个接口。主要作用是工厂类的方法中可以写逻辑。相应地,配置文件中将bean修改,需要写上对应两个属性表明要通过工厂中的哪个方法来创建bean。
不常用了,了解即可。
把静态方法改成实例方法,同时必须要为工厂也创建一个bean交给IoC容器管理,这样IoC容器才能通过实例工厂创建出接口的bean。
不常用了,有兴趣了解即可。
写一个工厂bean,这个bean一定要实现一个接口FactoryBean<>。然后重写方法返回bean对象和bean的接口class。配置文件中只需配置一个bean(指向FactoryBean),之后IoC容器就会到工厂中调用继承自FactoryBean的方法,创建出相应的接口对象。
问题来了,通过工厂bean来创建接口bean,接口bean默认就是单例的,工厂bean可以在<bean>
中通过属性来控制单例/非单例,如果要让接口bean非单例,应该怎么做呢?只需要再实现一个方法即可。
生命周期:从创建到销毁的整个过程。
控制bean的生命周期:在bean创建后到销毁前做一些事情。
主要有两个方法,一个用在bean被初始化时,一个用在bean被销毁时。直接在bean里面写init和destroy方法就可以了。然后要在配置文件对应的bean标签里写属性init-method把初始化的方法名填进去,destroy-method把销毁的方法名填进去。
不过销毁的方法由于程序往往会极其快速关闭,IoC还没关呢程序就结束了。所以要使用IoC容器调用一个close()
方法或者registerShutdownHook()
。这样就会先把IoC容器关闭将bean销毁再结束程序。
另外,也可以通过实现接口,重写方法的方式来控制bean的生命周期。
在单例模式下,IoC容器被创建的时候,所有bean都会被创建出一个对象存储在IoC容器之中。非单例模式下,IoC容器被创建的时候,bean不会被实例化对象。只有主动调用IoC容器创建bean,bean才会被创建。所以,单例模式下,IoC容器创建完,所有bean的生命周期也就开始了。
总结,bean的生命周期主要如下:
bean调用bean,或者bean使用数值字符串,需要使用依赖注入的方法来做。主要有四种方法:setter注入、构造器注入、自动装配(强大的方式)、集合注入
setter注入
bean1的成员是bean2。bean1不能直接new出bean2,因为耦合度太高了。可以这样做:只把属性摆上去,使用setter来给属性传递引用,这样bean2的对象就有了。使用setter注入需要具备数值属性或者bean属性(bean属性要写一个setter方法)。然后在配置文件中配置就可以了。如此一来,当bean1创建的时候,会创建一个bean2,然后spring调用setter方法将bean2的引用传递给bean1的属性。
xml文件要配置使用setter注入。在properties标签中,name属性是要被注入的成员的变量名(要给哪一个成员注入?)ref或者value是要注入的内容(要注入什么?)
构造器注入
bean1创建的时候,会调用bean1的构造方法,然后bean1的构造方法需要传入一个bean2。这时候spring会首先创建一个bean2,然后再创建bean1,这就是构造器注入。
xml文件要配置使用构造器注入。==构造器注入非常严谨,第三方框架大量使用。==如果受控bean没有setter方法就必须使用构造器注入。
依赖自动装配
IoC自动根据类型、或名字等去容器中寻找与当前要引入的依赖成员。
直接在bean标签里再加一个属性autowise即可。IoC识别到这个bean有依赖的时候,会自动在容器中寻找然后装配。这可以说是非常方便了。
然而也容易出现问题,如果要自动装配的bean不存在,必定报错;如果一个接口有两个实现类,就不能按照类型去自动装配了,而应该按照名称。
自动装配只能应用于引用类型,不能对简单类型进行注入操作。自动装配的优先级低于setter注入和构造器注入,如果同时出现,自动装配失效。
集合注入
如果要注入的bean是集合/数组,应该如何注入呢?集合/数组中存放什么数据?
在bean的标签体中写上属性标签,然后再在其中写入<array>/<list>/<set>/<map>/<properties>
等标签中的一个,然后再在这些标签中使用<value>简单类型
、<ref>引用类型
写入值。
开启context命名空间,在xml文件中写入一个标签<context>
。
使用context命名空间加载properties文件
<!--使用命名空间加载properties文件。system-properties-mode="NEVER"是说让里面的键不要被识别成系统变量-->
<context:property-placeholder location="classpath*:*.properties" system-properties-mode="NEVER"/>
加载容器,除了上述的方法以外,还有:
获取bean的方法还有:
IoC容器最早是由BeanFactory实现的,之后随着版本的迭代,功能的追加,才逐渐到ApplicationContext这一级。
在要定义成bean的controller、service、dao层的类中使用@Component,可以直接将这个类定义成bean。这时候不需要在配置文件中使用bean标签。但是配置文件中需要定义一个命名空间(就是上边的加载properties文件那个命名空间)。然后使用命名空间扫描对应包的所有类文件,通过反射哪些类添加了@Component注解,会将该类自动变成bean。
<--注意要在类上使用@Component注解 -->
<context:component-scan base-package="person.ghl"/>
为了便于区分,spring让@Component衍生出三种注解分别对应表现层、业务层、数据层:@Controller、@Service、@Repository。这三种注解的作用跟@Component的作用完全一样,只是名字不一样用于区分罢了。
@Controller、@Service、@Repository这三个注解后面加个括号写上id。也可以不加括号不写id,但是这样的话容器获取bean就必须使用类名的方式获取bean。而不能使用id的方法获取bean。
spring的思想就是简化开发。使用注解确实大大简化了开发,但是还有一个东西存在——xml配置文件。
能不能让xml配置文件也简化一下呢?
可以用Java类来代替配置文件。
新建一个包叫做config包,里面新建一个SpringConfig类,这个类使用注解@Configuration,于是这个类就是一个配置文件了。**再次使用注解@ComponentScan(“bean所在包”)扫描bean。**如果想要加载多个包,在注解参数里使用数组{“包名”,“包名”,“包名”}来进行加载。
与此同时,获取IoC容器就不能通过加载xml配置文件创建IoC容器对象来获取了。而是通过读取IoC配置类来创建对象:
注解方式配置bean
在bean的类上@Scope
:控制创建对象时创建单例对象还是非单例对象。(”prototype“)和(”singleton“)
bean的生命周期:在bean的方法上加上注解@PostConstruct/@PreDestory
。(注意是在方法上)
注解方式引入外部文件
在SpringConfig类上加上注解@PropertySource(”外部文件名称“)
来引入外部文件。有多个配置文件的时候使用数组。
注解方式实现依赖注入
首先是引用类型注入。
注解方式实现引用类型依赖注入是使用自动装配的方式进行的。只需要在成员上添加注解@Autowired
即可。IoC容器首选的装配方式是根据类型(接口的实现类的类型)进行装配。然而接口如果有多个的话肯定就报错了,因为IoC不知道要给接口装配哪一个实现类。如果bean的接口成员有多个实现类的话,可以在@Autowired下边在加上一个@Qualifier(”实现类bean的id“)
来确定具体装配的实现类。
接下来是基本类型注入
只需要在bean的成员上添加@valuie(”值“)注解就可以了。但是这样有点儿多余,直接让成员等于不就好了。所以要换一种写法,一般是使用外部文件给@value注入值。所以最后就是这样写:@value(”${}“)
。
第三方bean配置
第三方的bean可不会允许在类名上方加注解。
所以只能写一个方法,在方法中new出要获取的第三方的对象,给这个对象设置好属性,然后将对象返回。
最后,在这个方法上添加一个注解@Bean
,表示当前方法的返回值是一个bean,可以在IoC容器中找到的。
==这个方法要写在一个新的类XXConfig里面,不能直接写在SpringConfig里面,不然耦合度太高了。==但是如果写在新的类里边,就不能识别到了。所以,在SpringConfig类名上边还要再加一个注解@import({XX.class})
。
第三方依赖注入
如果第三方的bean需要一些依赖,也是需要手动set一下。
基本类型,第三方bean的Config类中字段写所有想要注入的成员,并使用注解@value(”${}“)。之后在方法里使用这些字段为第三方bean对象set值。
引用类型注入,这个比较特殊:不需要使用任何注解,直接在方法参数中写想要注入的bean即可。IoC会使用自动装配为形参传递引用。
最后,将自定义的Config类在SpringConfig类中引入:使用@Import注解引入。
spring的核心作用就是管理bean。
==只需要将实现类注解成bean就可以了接口不用注解成bean。==使用的时候只使用接口。因为spring会根据接口自动获取实现类注入接口。
首先,再来看看mybatis原生开发的核心代码。
mybatis的核心对象是SqlSessionFactory。这个对象应该成为一个bean被spring管理。这个对象是由mybatis配置文件(数据源信息和mapper所在包信息)得来的。这个配置文件里边大部分信息其实都可以交给框架进行管理(除了数据源和mapper所在包需要手动配置)。
mybatis为了和spring整合,专门写了一个包用于spring整合mybatis。这个包里最核心的就是SqlSessionFactoryBean类。只需要new出一个SqlSessionFactoryBean对象,然后再用setter设置其数据库连接池的属性(需要导入druid包,将druid数据库连接池也配置成bean),就没有必要再写mybatis配置文件了。
除了基本的spring-context包以及mybatis包以及mysql驱动包以外,需要导入三个包(spring操作数据库的包、mybatis用于整合入spring的包、druid数据源的包)。
导包完毕之后,如何整合?
其实很简单,新建一个MybatisConfig类,写两个方法让IoC托管mybatis的两个核心配置bean(一个bean负责数据库连接,一个bean用于代理开发包扫描),这样mybatis的核心配置就搞定了。不用写核心配置文件了。
然后:
pojo:表的实体类这个不必多说。
dao层:写接口并实现数据访问逻辑(注解开发或者代理开发)。让这个接口成为bean让IoC容器接管(之后如果需要用到这个接口的实现类进行注入,IoC容器会寻找mybatis的两个配置bean然后反射给这个接口提供实现类,完成注入)。应当注意,如果没有整合mybatis,该接口在其他地方是无法完成自动装配的,因为我们没有写该接口的实现类,实现类是mybatis通过动态代理技术创建的。
service层:写dao层对应的service接口并且写接口的实现类。让这个实现类成为bean让IoC容器接管。这个实现类中有dao层的接口对象,使用自动装配完成注入。然后写业务逻辑就可以了。
关键就在于将mybatis的核心配置文件变成两个bean并让IoC容器接管。然后就跟我之前学的mybatis操作没有什么区别了。甚至dao层的接口都不用写成bean,mybatis整合入spring,那么它创建的代理对象必定也是已经注册成bean的了,自动装配的时候spring通过接口在IoC容器中寻找实现类,是可以找到的。
AOP即面向切面编程。它的作用是在不惊动原始设计的基础上为程序进行功能加强。
核心概念:
导入坐标:由于spring-aop的坐标在导入spring-context的时候就已经导入了,所以不需要导入。只需要导入aspectjweaver的坐标就可以了。
接着,写一个定义一个切面(一个类)。
然后,切面中定义切入点和通知的关系。切面中**写一个私有的void无参数方法,用于绑定切入点方法,并且上边添加一个注解@Pointcut("execution(void 切入方法所在位置)")
。**然后,写一个通知方法(这个通知方法就是用来写逻辑的),通知方法要和切入点绑定在一起,所以通知方法上边也要添加一个注解@Before(“私有返回void无参数方法名()”)。
最后,切面上边要写注解@Component
和@Aspect
,让IoC能够识别为bean且能识别为切面。且在SpringConfig类名上边要加一个注解@EnableAspectJAutoProxy让IoC开启识别切面的模式。
配置完毕,切入点方法会实现通知的功能但是不会影响原本切入点方法的设计。
IoC容器启动
读取所有切面配置中的切入点
初始化bean,确定所有bean对应的类中方法是否匹配到切入点
bean匹配失败,创建bean的原始对象,调用原始对象的方法
bean匹配成功,创建bean原始对象的代理对象,调用通知方法和原始对象的方法实现功能
切入点绑定方法上的注解@PointCut()
的参数是一个切入点表达式。
切入点表达式的标准格式:
@PointCut(execution(修饰符 返回值 方法(参数) 异常名))
//修饰符和异常名都可以省略
//其实这个东西就是用来精准匹配的嘛
可以使用通配符描述切入点表达式(切入点表达式中的每一个东西都可以替代):
*
:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀匹配符出现..
:多个连续的任意符号,可以独立出现,常用于简化包名和参数的书写+
:专用于匹配子类类型
excution(* *..*Service+.*(..))
,修饰符任意,返回值任意,包名任意,类名以Service结尾,必须是其子类,方法任意,参数任意。一般来说,空方法绑定的切入点方法是接口里的方法(当然实现类中的也可以),但是绑定了接口方法那所有的实现类也可以使用了。
通知类型有三种:
@Before
:在切入点方法之前嵌入通知,通知方法可以传入一个JoinPoint对象,用于获得切入点方法的信息
@After
:在切入点方法之后嵌入通知,通知方法可以传入一个JoinPoint对象,用于获得切入点方法的信息
@Around
:在切入点方法之前和之后嵌入通知
@Around的使用和before以及after有一些不同。使用@around会默认把切入点方法覆盖。如果不想覆盖,需要给通知方法传入ProceedingJoinPoint对象(继续连接点),然后在Around型通知方法中调用ProceedingJoinPoint对象的proceed方法继续连接点方法的操作。值得注意的是:如果这样调用了原始连接点方法的操作,而且原始连接点方法有返回值,必须让通知方法有一个Object的返回值,然后proceedJoinPoint.proceed()返回的值再返回就可以了(不用强转,就直接返回一个object就可以,之后再在业务中进行强转)。而且由于无法预知连接点方法是否会抛异常,所以通知方法也要抛异常。
@AfterThrow:连接点抛出异常后嵌入通知
一般来说,最好用的就是@Around,因为它可以实现@After和@Before的效果。
proceedJoinPoint就是切入点方法,所以它可以获取连接点的信息。先用getSigature获取执行的签名信息,再用sigature.getDeclaringTypeName()获取包类名,用sigature.getName()获取方法名。可以使用proceedJoinPoint.getArgs获取连接点方法的参数(获取到之后可以修改!!真正的暗箱操作)。
事务:要么同时成功,要么同时失败,不会出现成功了一半这种现象。成功了就提交,中间出现异常了就回滚到原始状态。spring事务能够保证数据层或者业务层中一系列的数据库操作同时成功或者同时失败。
三步开启事务:
打个比方,银行转账业务。转账业务是在service中的,转账业务需要调用dao层中两个方法:入账和出账。
转账业务就是一个事务管理员,入账和出帐就是事务协调员。
而事务传播行为,就是设置事务管理员以及事务协调员之间关系的。当传播属性是REQUIRED时,事务协调员的事务会加入事务管理员的事务。当传播属性时REQUIRE_NEW时,事务协调员不会加入事务管理员的事务。具体见下表。