Spring全家桶
- Spring Framework
核心 - Spring Boot
构建项目,在其基础上开发更容易更方便 - Spring Cloud
微服务,当一个规模较大的项目维护起来比较困难时,可以利用spring cloud将项目拆分成若干个子项目,然后把他们再集成在一起,每一个子项目规模小,就便于维护,但是开发难度会有所提高 - Spring Cloud Data Flow
是spring做数据集成的功能,比如一个应用有很多客户端,包括pc端、移动端、可穿戴设备、汽车、传感器等,这些客户端所采集的数据形态各异,怎样把这些数据集成整合在一起形成更有价值的数据就是Spring Cloud Data Flow所作的事情
此项目不会把这四大功能都用上,项目本身就是某网的一个模块,没有必要往下拆分,微服务就不用去做了,同时这个项目就只做浏览器客户端,用不到data flow。只用到spring framework和spring boot。spring全家桶的使用手册在https://spring.io上可以查到
Spring具体介绍:
1.Spring Framework
本项目中用到的四大部分
- Spring Core
- IoC(用来管理Bean的思想,面向对象的)、AOP (面向界面)
- 可以说spring的其他所有功能都是以Ioc为基础的,因此要先学好Ioc
- 有了这两种思想能够管理一切Bean,哪怕把一个第三方的框架拿过来,它也能对其进行整合。spring厉害就厉害在它并非功能多,而是它这两种管理Bean的思想能够管理一切。
- Spring Data Access (spring访问数据库的功能)
- Transactions(管理事务)、Spring MyBatis(整合mybatis)
- Web Servlet (web开发)
- Spring MVC
- Integration (集成)
- Email(发邮件)、Scheduling(定时任务)、AMQP(消息队列发送消息)、Security(安全控制、权限管理)
、
2.Spring IoC
- Inversion of Control
- 控制反转,是一种面向对象编程的设计思想。
比如,当我们自己管理对象时,通常是new A,new B,A.set传入B,这样A和B就有了联系,能够通过A去调用B,这种做法比较直观,但是缺点是A与B产生了耦合,直接耦合在一起,当项目规模比较大时,可能就会不容易去维护,若想对A做出更改,则需要把B也更改;那么IoC思想是通过别的办法简化Bean之间的关系,从而减少Bean之间的耦合度,让项目能够便于维护。
实现这种思想的方式:依赖注入。
依赖注入的实现基于IoC容器。
- 控制反转,是一种面向对象编程的设计思想。
- Dependency Injection
- 依赖注入,是IoC思想的实现方式。
- IoC Container
-
IoC容器,是实现依赖注入的关键,本质上是一个工厂。
-
代码示例:
类CommunityApplication的代码如下
package com.nowcoder.community;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CommunityApplication {
public static void main(String[] args) {
SpringApplication.run(CommunityApplication.class, args);
}
}
下面解释一下这个类运行时做了哪些事情:
- main方法只有一句话:运行Spring应用
- 底层启动了tomcat,但是底层不只是启动了tomcat,也自动创建了spring容器。web项目当中spring容器不需要我们主动去创建而是被自动创建的
- spring容器被创建以后,它会自动去扫描某些包下的某些bean,将这些bean装配到容器里,至于哪些bean会被扫描到,需要看配置类
- 由代码
SpringApplication.run(CommunityApplication.class, args);
知,这个类CommunityApplication是run方法的参数,spring应用在启动的时候是需要配置的,因此这个类其实就是一个配置文件 - 注解
@SpringBootApplication
所标识的类就表明这个类就是一个配置文件
按Ctrl并点击该注解可以进入注解底层查看,这个注解是由其他注解所组成的
如下图:
对注解@ComponentScan
注解的进一步说明:
CommunityApplication这个配置类所在的包com.nowcoder.community和这个包的子包controller,这些包中的bean都会被扫描到,但是bean上需要有@controller
注解才能够被扫描。
与@Contoller
功能相同的注解还有@Service
、@Component
说明:
navigate->classes(或者Ctrl+N) 搜索Service,打开后可以看到@Service
是由@Component
来实现的,因此注解用@Component
也同样可以实现。@Controller
也是由@Component
实现的,@Repository
也有相同的功能。
总结:
@Controller
@Service
@Component
@Repository
四个注解都可用于将其加到某一类上,然后使这个类被容器所扫描。区别在于语义上的区别:
开发业务组件--
@Service
处理请求--
@Controller
数据库访问组件--
@Repository
通用--
@Component
代码演示IoC的使用方式:
在测试类中进行演示
然而IDEA测试类使用
@RunWith
注解时发现并没有这个注解,此时查看导入的test依赖(pom.xml)
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
解决办法:把代码删掉即可正常使用,既导入的test依赖如下
org.springframework.boot
spring-boot-starter-test
test
保存后重新在CommunityApplicationTests.java中引用注解
@RunWith(SpringRunner.class)
在测试类中若想引用正式类中的配置类,则需要引入注释
@ContextConfiguration(classes = CommunityApplication.class)
那么测试代码则能以CommunityApplication.class为配置类进行配置。
IoC是个自动创建的核心Spring容器,那么如何去得到这个容器呢:哪个类想得到Spring容器,就去实现这样一个接口:在类名后添加implements ApplicationContextAware
set方法,传入一个参数叫做applicationContext,这个参数实际就是Spring容器对象(对applicationContext的理解详见文章Spring容器和应用上下文)应用上下文,可以简单的理解成就是将你需要Spring帮你管理的对象放入容器的一种容器对象,本质上说就是一个维护Bean定义以及对象之间协作关系的高级接口。Ctrl+click查看applicationContext底层
同样再查看ListableBeanFactory和HierarchicalBeanFactory底层,发现他们都继承于BeanFactory
由此可见,applicationContext是继承了BeanFactory后派生而来的,而BeanFactory就是Spring容器的顶层接口,ApplicationContext接口,它由BeanFactory接口派生而来,ApplicationContext包含BeanFactory的所有功能,功能更强大,通常建议比BeanFactory优先使用。
如果一个类实现了ApplicationContextAware接口,Spring容器会自动调用ApplicationContextAware接口中的setApplicationContext方法,在当spring容器初始化的时候,会自动的将ApplicationContext注入进来,通过这个上下文环境对象(ApplicationContext)得到Spring容器中的Bean
为了暂存spring容器,在测试类当中加入一个成员变量用于记录容器
private ApplicationContext applicationContext;
因而当程序运行的时候,application就可以被自动传进来并被记录,那么就可以使用在别的地方了。
下面写一个测试方法,在测试方法中去使用spring容器,代码如下:
@Test
public void testApplicationContext() {
System.out.println(applicationContext);
}
然后运行testApplicationContext(),报错,原因是test包导错了,在@Test有两个包,一个是org.junit.jupiter.api.Test,另一个是org.junit.Test,而测试需要的Junit是org.junit.Test。因此将 import org.junit.jupiter.api.Test; 替换为 import org.junit.Test,重新运行。
运行结束后查看对象的输出形式,如图
GenericWebApplicationContext@5e3d57c7
对象的输出形式是:类名@hashcode
总之就是证明了这个容器是存在、可见的;
下面演示如何去用这个容器来管理bean,那么首先得有bean让其去管理,写一个bean。新建一个包,用于存放访问数据库的bean,给这个包命名为dao(data access object 数据库访问对象)
然后在dao包下创建访问数据库的接口new->java class->选择Interface()接口类
接口中需要定义一些方法
public interface AlphaDao {
String select();
//定义select查询方法,没有参数,返回一个字符串
}
接口的实现需要有实现类,那么再创建一个该接口的实现类AlphaDaoHibernateImpl
,命名解释:这个实现类采用Hibernate技术来实现(implement)查询。接下来在这个类中实现接口:implements AlphaDao,然后Ctrl+I增加接口中的方法。实现类创建情况如下图
但是这个bean还不能被容器管理,或者说容易还不能扫描并装配这个bean,因此需要引入前面提到的访问数据库的注解@Repository,如此spring容器就可以自动扫描到这个bean然后把它装配到容器里。
回到测试类中,
@Test
public void testApplicationContext() {
System.out.println(applicationContext);
AlphaDao alphaDao = applicationContext.getBean(AlphaDao.class); //getBean()可以通过名字或类型获取,这里是通过类型获取
//从容器里获取自动装配的bean
System.out.println(alphaDao.select()); //调用查询方法并将结果输出出来
}
运行结果如图:
成功返回了Hibernate,说明成功地获取到了bean(AlphaDaoHibernateImpl.java)并得到了查询的结果。
下面再创建一个bean,通过这个例子来体现spring容器的优势。比如当项目发展到一定阶段,引入了新技术Mybatis,假设mybatis比Hibernate更有优势,想要把项目中的Hibernate替换成mybatis,做法是在dao项目中创建一个新的实现类AlphaDaoMyBatisImpl
,同样去实现接口即implements AlphaDao,并增加接口待实现方法,返回"MyBatis"字符串,同时加上注解@Repository
。
但是若这时候去运行测试中的testApplicationContext()方法还存在问题,问题在于getBean()是要按照类型获取bean,如:getBean(AlphaDao.class)
,这个括号中的类型是一个接口 ,那么这个接口满足条件的bean有两个--AlphaDaoMyBatisImpl和AlphaDaoHibernateImpl,那么spring容器就不知道该用哪个bean了。解决办法:在要用到的bean上加上注解@Primary
,即注明这个bean有更高的优先级。(这里将AlphaDaoMyBatisImpl定为最高优先级)
运行testApplicationContext(),运行成功后返回了MyBatis
也就是说,原先测试程序当中dao的实现类都是由HibernateImpl来实现的,当需要替换掉bean的时候,只需要再创建/添加一个bean,并在此新添加bean上加一个@Primary
注解就可以了,测试程序中调用的地方并不需要发生变化,因为调用的不是bean本身而是依赖bean的接口,因此即使是实现类变了,调用代码本身是不用改的。因此,spring容器就是用这种方法降低了bean之间的耦合度,调用方和实现类不会发生任何直接的关系。
但是新的问题是若程序某一个程序块仍想用Hibernate而不想用mybatis,如何去实现呢?bean默认的名字是类名(首字母小写),当然也可以自定义bean的名字,在bean必须有的注解后边用("字符串")
的形式另外命名,例如@Repository("alphaHibernate")
,那么这个bean的名字就命名为了alphaHibernate。当出现一个程序中想调用两个bean的时候就通过名字强制容器返回非primary的bean。
@Test
public void testApplicationContext() {
System.out.println(applicationContext);
AlphaDao alphaDao = applicationContext.getBean(AlphaDao.class); //getBean()可以通过名字或类型获取,这里是通过类型获取
//从容器里获取自动装配的bean
System.out.println(alphaDao.select()); //调用查询方法并将结果输出出来
//下面面重新获取一次bean
alphaDao = applicationContext.getBean("alphaHibernate", AlphaDao.class);
//通过名字而非类型获取bean的默认返回类型为object,因此需要强制转型,或者再加一个类型参数(如上)
//意在说明将object类型转换为AlphaDao.class
System.out.println(alphaDao.select());
}
运行结果:
以上就是我们从容器中获取bean的基本方式。
那么容器管理bean不仅是能帮我们创建bean,还有更多管理bean的手段,下面演示spring容器管理bean更多的方法。
spring容器除了创建bean还能帮助管理bean的初始化以及销毁bean。先创建一个新的包service用于存放开发的业务组件,然后在此包中创建类AlphaService。
在具体Bean的实例化过程中,@PostConstruct注释的方法,会在构造方法之后,init方法之前进行调用。为了便于观察@PostConstruct注释的方法调用的时刻是否是在构造方法之后,再给类AlphaService加一个构造方法AlphaService进行对比。除了管理初始化方法还能管理销毁方法,那么再给这个类加一个销毁方法。
@Service
public class AlphaService {
public AlphaService() {
System.out.println("实例化AlphaService");
}
@PostConstruct
//要想让容器帮助管理这个方法,就是让容器在合适的时候自动调用这个方法,那么就加上这个注解
//@PostConstruct注释的方法,会在构造方法之后,init方法之前进行调用
public void init() {
System.out.println("初始化AlphaService");
}
@PreDestroy
//当Bean在容器销毁之前,调用被@PreDestroy注解的方法
public void destroy() {
System.out.println("销毁AlphaService");
}
}
进行测试,看一看能不能通过容器自动调用初始化以及销毁的方法。回到测试类,再写一个新的测试方法。
@Test
public void testBeanManagement() {//测试bean管理的方式
//通过容器去获取Service,看会出现什么样的情况
AlphaService alphaService = applicationContext.getBean(AlphaService.class);
System.out.println(alphaService);
}
运行testBeanManagement() 方法,结果如下图:
看控制台,最终确实打印出了该对象,说明这个bean可以被实例化。再看程序启动过程输出的内容,包括spring容器创建的过程,在这个过程当中会看到这样的输出:
可以说明bean的初始化方法确实是在构造器之后被调用的,销毁方法是在对象销毁之前(程序结束之前)调用的。在初始化和销毁之间打印出了bean的实例。
在程序启动时bean被实例化,在程序停止的时候bean被销毁,说明bean只被实例化一次、只被销毁一次,在容器中只有一个实例,即bean是单例。因为main方法只启动一次,随意bean只被实例化一次,那就只能有一个实例。下面再通过测试来验证这个说法:再来获取一下bean并打印,看最终实例化了几次。
@Test
public void testBeanManagement() {//测试bean管理的方式
//通过容器去获取Service,看会出现什么样的情况
AlphaService alphaService = applicationContext.getBean(AlphaService.class);
System.out.println(alphaService);
//再来获取以下bean并打印,看最终实例化了两次还是一次
alphaService = applicationContext.getBean(AlphaService.class);
System.out.println(alphaService);
}
控制台打印结果为:
可见两次输出hashcode是相同的,因此是一个对象。
日志显示也只实例化了一次,说明被spring容器管理的bean默认是单例的。
若想要每次getBean()的时候都能新建一个实例(多例),就需要在bean上加一个注解@Scope("XXX")
Scope的默认参数是singleton(表示单例),prototype(多例),当加上这个表示多例的注解后,每次访问bean就会给我们创建一个新的实例。
因此在AlphaService中加入@Scope("prototype")
回到测试类中,运行testBeanManagement(),控制台输出结果如下:
这次并不是启动时初始化,而是每次getBean()都实例化一个对象,实例化两次就是两个bean,hashcode也不同。但是通常很少使用多例的形式。
以上用spring容器管理的都是我们自己写的bean,若在容器中装配第三方bean,就不能在这个第三方包中随意加注解(总是就是不能更改别人写的包),因此这时候就需要自己写一个配置类,在配置类当中通过bean注解进行声明,从而来解决这个问题。下面演示装配第三方bean、自己写配置类的操作方法:
随着项目的展开,以后会有更多的配置类将会放到同一个包下。新建一个包命名为config(配置),此包下创建一个配置类实例AlphaConfig.java。标识注解@Configration
表示这个类是一个配置类不是普通类。要定义第三方bean则需要加入注解@Bean
,同时定义方法。
@Configuration
public class AlphaConfig {
@Bean
//SimpleDateFormat方法返回的类型就是SimpleDateFormat,实例化一次就可以调用多次
//注意:方法名simpleDateFormat就是Bean的名字
public SimpleDateFormat simpleDateFormat() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//实例化simpledateformat,并给它指定一个格式
}
}
//这段话的意思就是这个方法返回的对象将被装配到容器中,这个bean的名字就是simpleDateFormat。
回到测试类中,重新写一段测试代码:
@Test
public void testBeanConfig() {
SimpleDateFormat simpleDateFormat = applicationContext.getBean(SimpleDateFormat.class);
System.out.println(simpleDateFormat.format(new Date())); //格式化当前日期
}
运行结果:
spring容器基本就是这样去使用的,但以上这些方法都是通过我们主动操作去获取容器中的bean,但是这种方法较为麻烦,spring还有更简便的使用容器的方式。即主动获取。通过以上比较笨拙的方式去理解了spring容器是什么、它的底层有什么方法、怎样直接去使用它。
事实上在日常开发使用spring容器过程中是通过依赖注入的方式以一种更便捷的方式去使用spring容器获取bean。
下面演示的过程用于说明什么是依赖注入。若当前的bean要使用AlphaDao,则没有必要去通过容器的get方法获取,而只需要声明给当前的bean注入AlphaDao就可以,注入要使用注解@Autowired
,后边紧接成员变量
对@Autowired
的解释:
掘金--彻底搞明白Spring中的自动装配和Autowired
从Spring2.5开始,开始支持使用注解来自动装配Bean的属性。它允许更细粒度的自动装配,我们可以选择性的标注某一个属性来对其应用自动装配。
Spring支持的几种不同的应用于自动装配的注解中,Spring自带的@Autowired注解是其中的一种。
使用@Autowired很简单,在需要注入的属性加入注解即可。
@Autowired
UserService userService;
那么Autowired是按照什么策略来自动装配的呢?
关于这个问题,不能一概而论,你不能简单的说按照类型或者按照名称。但可以确定的一点的是,它默认是按照类型来自动装配的,即byType。
@Autowired默认使用byType来装配属性,如果匹配到类型的多个实例,再通过byName来确定Bean。
对自动装配的解释:
Spring提供了4种自动装配策略
1、byName
它的意思是,把与Bean的属性具有相同名字的其他Bean自动装配到Bean的对应属性中。
2、byType
如果不使用属性名称来对应,你也可以选择使用类型来自动装配。它的意思是,把与Bean的属性具有相同类型的其他Bean自动装配到Bean的对应属性中。
3、constructor
它是说,把与Bean的构造器入参具有相同类型的其他Bean自动装配到Bean构造器的对应入参中。值的注意的是,具有相同类型的其他Bean这句话说明它在查找入参的时候,还是通过Bean的类型来确定。
4、autodetect
它首先会尝试使用constructor进行自动装配,如果失败再尝试使用byType。不过,它在Spring3.0之后已经被标记为@Deprecated。
5、默认自动装配
默认情况下,default-autowire属性被设置为none,标示所有的Bean都不使用自动装配,除非Bean上配置了autowire属性。
如果你需要为所有的Bean配置相同的autowire属性,有个办法可以简化这一操作。
在根元素Beans上增加属性default-autowire="byType"。
测试类中代码如下:
@Autowired
private AlphaDao alphaDao;
//让spring容器将AlphaDao注入给属性alphaDao,于是该属性便可直接使用
@Autowired
private AlphaService alphaService;
@Autowired
private SimpleDateFormat simpleDateFormat;
@Test
public void testDI() { //DI--Dependency Injection 依赖注入
//在测试方法中直接使用成员变量,看能不能直接取到这个bean
System.out.println(alphaDao);
System.out.println(alphaService);
System.out.println(simpleDateFormat);
}
运行testDI(),运行结果如下:
若想要alphaDao注入的不是默认优先级Mybatis而是Hibernate,就需要再加一个注解@Qualifier("alphaHibernate")
,那么spring容器就会把名为alphaHibernate的bean注入给他。
spring以依赖注入的方式管理bean,只需要声明一个属性,写一个注解,bean就可以拿来用了。若bean依赖的是接口,也会降低耦合度。依赖注入的实现方式简单方便。注解也可以加在类构造器前,通过构造器注入;或者加到set方法之前,通过set方法注入;但通常都是加到属性之前。
那么在实际开发过程中怎样去运用spring容器的依赖注入思想?下面进行综合的演示。
开发过程中由controller来处理浏览器的请求,它在处理浏览器请求的过程中会调用业务组件去处理当前的业务,业务组件会调用dao(data access object)去访问数据库。即controller调用service,service调用dao,他们是互相依赖的,依赖关系就以依赖注入的方式来实现。
Service调用dao:
@Service
//@Scope("prototype") //每次访问bean就会给创建一个新的实例(多例)
public class AlphaService {
@Autowired
private AlphaDao alphaDao;
public String find() {
return alphaDao.select(); //调用alphaDao来实现查询,直接返回查询结果
} //Service依赖于dao的方式
}
controller调用service:
@Controller
@RequestMapping("/alpha") //(“/alpha”)是给类取一个访问的名字,浏览器通过这个名字来访问类
//以上两个注解是spring MVC的注解
public class AlphaController {
@Autowired
private AlphaService alphaService;
@RequestMapping("/data")
@ResponseBody
public String getData() {
return alphaService.find(); //把find的结果返回给浏览器
//浏览器访问这个方法的前提是得有注解声明
}
}
之后运行正式的代码CommunityApplication,然后打开浏览器访问localhost:8080/alpha/data
controller调用了service的find()方法,而find()调用的是dao的select()方法,dao默认注入的是优先级高的mybatis。
End