最近在学习松哥的Spring系列,真的挺好入门的,本篇包含自己的学习记录和理解。
我们常说的 Spring 实际上是指 Spring Framework,而 Spring Framework 只是 Spring 家族中的一个分支而已。其实Spring家族还有好多东西:
Spring是为了解决企业级应用开发的复杂性而创建的。
一般来说,初学者主要掌握Spring四个方面的功能:
- IoC/DI
- AOP
- 事务
- JdbcTemplate
这一篇先来介绍IoC,下一篇介绍后三个
文章目录
一、IoC
1. IoC概念
控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做**依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
以上是百度百科说的,用人话来说就是:
我们原来是在用new的方式获取对象,是主动的。而现在,我们获取对象是找工厂要的,工厂为我们创建或查找对象,是被动的。但是为啥叫控制反转,而不叫降低依赖呢?因为本来创建业务接口新对象的控制权是在业务方法里面的,它可以选择创建或不创建,但是现在它转向请求工厂干这个事。所以说等于把控制权给了工厂,叫控制反转。
再简单点说:
对象创建的控制权给了工厂(spring容器),我们就不用自己new了,找它要就行!
而需要注意的是,IoC不解决别的,只解决一个问题:降低耦合!!!
2. 小试牛刀
- 创建maven项目,引入spring-context依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
</dependencies>
- 添加Bean。在resources目录下创建一个spring配置文件,添加需要的Bean,即将其注册到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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.javaboy.Book" id="book"/>
</beans>
- 启动配置。用一个main加载配置方法(通过新建ClassPathXmlApplicationContext对象),通过getBean方法获取对象
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Book book = (Book) ctx.getBean("book");
System.out.println(book);
}
}
- 自写,三种方法获取容器对象,前两种用的比较多
public class Main {
public static void main(String[] args) {
//拿到容器的上下文
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
//用下面这个也行,只是要绝对路径,用的比较少
//FileSystemXmlApplicationContext ctx = new FileSystemXmlApplicationContext("D:\\WorkProject\\javaStudy\\spring\\IoC01\\src\\main\\resources\\applicationContext.xml");
//三种方法获取容器内的user对象
User user1 = (User) ctx.getBean("user");
User user2 = ctx.getBean("user", User.class);
//直接用类来创建,只能创建一个对象,用的比较少
User user3 = ctx.getBean(User.class);
System.out.println(user1);
System.out.println(user2);
System.out.println(user3);//三个对象输出的是用一个地址,说明是同一个对象
}
}
3. 属性注入
基本属性注入:基本数据类型
复杂属性注入:对象、数组、Map、Properties
3.1 构造方法注入
<?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="org.jacob.ioc.model.User" id="user">
<constructor-arg name="id" value="2019"/>
<constructor-arg name="username" value="baihui"/>
<constructor-arg name="address" value="www.jacob.org"/>
</bean>
</beans>
构造方法中name是属性名称,value给其赋值;name也可以是0、1、2的标号,代表构造函数的第几个参数,属性含义不明确,不建议使用。
3.2 set方法注入
xml配置文件中改为标签(最常用)
<bean class="org.jacob.ioc.model.User" id="user2">
<property name="id" value="002"/>
<property name="address" value="www.jacob.com"/>
<property name="username" value="一块石头"/>
</bean>
主方法中用第二种取对象
User user2 = ctx.getBean("user2", User.class);
System.out.println(user2);
3.3 p名称空间注入
只是写法不同,本质上还是set(用得不多,了解即可)
<bean class="org.jacob.ioc.model.User" id="user3" p:username="jacob" p:id="003" p:address="www.jacob.org"/>
User user3 = ctx.getBean("user3", User.class);
System.out.println(user3);
3.4 外部Bean注入
如果一个类的构造方法是私有的,想交给容器管理,怎么办呢
以下为okhttp请求实例:
public class OkHttpTest {
public static void main(String[] args) {
//创建一个客户端对象
OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
//写请求
Request request = new Request.Builder()
.get()
.url("http://www.baidu.com")
.build();
//用client调用请求,拿到call对象
Call call = okHttpClient.newCall(request);
//请求回调,成功或失败的情况
call.enqueue(new Callback() {
//是被就打印异常信息
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
System.out.println(e.getMessage());
}
//成功就打印body信息
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
System.out.println(response.body().string());
}
});
}
}
静态工厂注入、实例工厂注入
3.5 静态工厂注入
新建一个工厂类,用静态方法创建http对象
public class OkhttpStaticFactory {
private static OkHttpClient okHttpClient;
//用静态方法new与一个build对象,把这个方法交给spring容器
public static OkHttpClient getInstance() {
if(okHttpClient == null){
okHttpClient = new OkHttpClient.Builder().build();
}
return okHttpClient;
}
}
由于是静态工厂,直接注入其getInstence方法
<bean class="org.jacob.ioc.OkhttpStaticFactory" factory-method="getInstance" id="okHttpClient"/>
主函数
//创建一个客户端对象
//OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
OkHttpClient okHttpClient = ctx.getBean("okHttpClient", OkHttpClient.class);
3.6 实例工厂注入
新建一个工厂类,用非静态方法创建http对象
public class OkHttpFactory {
private OkHttpClient okHttpClient;
public OkHttpClient getInstance(){
if(okHttpClient == null){
okHttpClient = new OkHttpClient.Builder().build();
}
return okHttpClient;
}
}
由于要执行build方法必须要有工厂的实例对象,所以先注入工厂对象,再注入getInstence方法
<!-- <bean class="org.jacob.ioc.OkhttpStaticFactory" factory-method="getInstance" id="okHttpClient"/>-->
<bean class="org.jacob.ioc.OkHttpFactory" id="okHttpFactory"/>
<bean class="okhttp3.OkHttpClient" factory-bean="okHttpFactory" factory-method="getInstance" id="okHttpClient"/>
主函数不变
4. 复杂属性注入
4.1 对象注入
在User类中加一个内部类Cat,xml实现对象注入,通过ref
来引用一个对象
<bean class="org.jacob.ioc.model.Cat" id="cat">
<property name="age" value="3"/>
<property name="name" value="小白"/>
</bean>
<bean class="org.jacob.ioc.model.User" id="user4">
<property name="id" value="004"/>
<property name="address" value="www.jacob.com"/>
<property name="username" value="jacob"/>
<property name="cat" ref="cat"/>
</bean>
4.2 数组注入
在User对象中加入一个Cat数组和爱好StringList,xml实现数组注入在标签中加或者标签,可以用ref直接引用已定义的bean对象,也可以直接在数组标签内部写bean信息
<bean class="org.jacob.ioc.model.Cat" id="cat">
<property name="age" value="3"/>
<property name="name" value="小白"/>
</bean>
<bean class="org.jacob.ioc.model.User" id="user4">
<property name="id" value="004"/>
<property name="address" value="www.jacob.com"/>
<property name="username" value="jacob"/>
<property name="cat" ref="cat"/>
<property name="cats">
<array>
<ref bean="cat"/>
<bean class="org.jacob.ioc.model.Cat" id="cat2">
<property name="name" value="小黑"/>
<property name="age" value="5"/>
</bean>
</array>
</property>
<property name="favourites">
<list>
<value>吃饭</value>
<value>睡觉</value>
<value>打豆豆</value>
</list>
</property>
</bean>
4.3 Map注入
<property name="details">
<map>
<entry key="gender" value="男"/>
<entry key="age" value="12"/>
</map>
</property>
4.4 Properties注入
<property name="info">
<props>
<prop key="phone">12345678</prop>
</props>
</property>
5. Java配置
在Spring中,想要将一个Bean注册到Spring容器上,整体上来说,有三种方式:
- XML注入,如上文示例
- Java配置(通过Java代码将Bean注册到Spring容器中)
- 自动化扫描
在传统的ssm项目中使用大量XML配置,而在Spring Boot中,不使用一行XML配置,都是Java配置
创建一个类SayHello,有个方法是sayhello
public class SayHello {
public String sayHello(String name) {
return "hello" + name;
}
}
创建一个JavaConfig类,使用@configuration注解,配置一个类SayHello,使用@Bean注解
//@Configuration注解,表示这是一个Java配置类,配置的作用类似于applicationContext.xml
@Configuration
public class JavaConfig {
//可以通过加参数给类配置别名,获取对象时用这个别名
//@Bean("sh")
@Bean
SayHello sayHello(){
return new SayHello();
}
}
另外,在@Bean注解后加参数可以给类配置别名,用以getBean方法中的第一个参数获取对象
用JavaConfig文件创建上下文ctx,getBean找他拿对象,调用方法
public class JavaConfigTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
SayHello sayHello = ctx.getBean("sayHello", SayHello.class);
System.out.println(sayHello.sayHello("world"));
}
}
6. 自动化配置
6.1 自动配置相关注解
上文可以看到,每个类都是我们手动输入到XML文件或JavaConfig文件中,这样有些繁琐,而实际开发中,大量使用自动化配置
可以使用Java配置,或XML配置来实现
例如将UserService类,使其能够自动注册到Spring容器中去,给其添加一个@Service注解,类似的注解一共四个:
- @Component
- @Repository
- @Service
- @Controller
都是基于Component做出来的,其功能也一致,那为啥要搞三个?主要是为了在不同的类上添加方便。
- 在Service层,添加注解使用@Service
- 在Dao层,添加注解使用@Repository
- 在Controller层,添加注解使用@Controller
- 在其他组件上添加注解,使用@component
6.2 java自动配置
在你想要自动注册的类外加上述注解,如下
@Service("us")//可以默认名字,也可以加参数起别名
public class UserService {
public List<String> getAllUser() {
List<String> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
users.add("javaboy:" + i);
}
return users;
}
}
在JavaConfig类外加一个@Configuration表示Java配置,再添加一个注解@ComponentScan,代表要自动化扫描的包的位置(如果不做指定,默认就是这个类所在包下的所有子类):
//@Configuration注解,表示这是一个Java配置类,配置的作用类似于applicationContext.xml
@Configuration
@ComponentScan(basePackages = "org.jacob.ioc")//注意这个包要再往后退一级
public class JavaConfig {
}
两个要注意的问题:
- Bean的名字:默认是类名的第一个字母小写,也可以在@Service后加参数起别名;
- 扫描位置和类型控制:除了按照包的位置来扫描,还有另外一种方式,就是根据注解来扫描。例如如下配置:
@Configuration
@ComponentScan(basePackages = "org.jacob.javaconfig",useDefaultFilters = true,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Controller.class)})
public class JavaConfig {
}
-
useDefaultFilters若是true,扫描四种类型,若是false,一个都不扫
-
includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Service.class)})
代表多扫描一个@Service注解 -
excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Controller.class)})
代表不扫描@Controller注解
故上述配置表示扫描 org.jacob.javaconfig 下的所有 Bean,但是除了 Controller。
6.3 XML自动配置
在applicationContext.xml中加上自动扫描的包和类型限定:
<context:component-scan base-package="org.jacob.ioc.Service" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
</context:component-scan>
表示不扫描 org.jacob.javaconfig 下的所有 Bean,但是除了@Service类型。
6.4 对象注入
自动扫描时的自动配置有三种方式:
- @Autowired
- @Resources
- @Injected(不常用)
如果只有一个对象就用@Autowired,多个对象就用@Resources
新建一个dao包,包下新建一个UserDao类
@Repository
public class UserDao {
public String hello(){
return "hello";
}
}
在想用它的类下加入内部UserDao类,并加入@Autowired注解,可以实现内部类的注入
@Service("us")//可以默认也可以加别名
public class UserService {
@Autowired//添加注解使内部类自动注入
UserDao userDao;
public void sayHello(){
String hello = userDao.hello();
System.out.println(hello);
}
}
7. 条件注解
根据当前的环境来注册不同的实例,在满足某一条件下生效的注解,在boot里应用非常多
7.1 条件注解
例子:自动检测当前系统环境,在Windows输出dir,在Linux输出ls
新建ShowCmd接口
public interface ShowCmd {
String showCmd();
}
写两个实现,分别返回"dir"和"ls"
public class WindowsShowCmd implements ShowCmd{
@Override
public String showCmd() { return "dir";}
}
public class LinuxShowCmd implements ShowCmd{
@Override
public String showCmd() { return "ls";}
}
写两个Condition判断系统环境,如WindowsCondition判断本机环境中包含"win"字段则返回true
public class WindowsCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
String osName = conditionContext.getEnvironment().getProperty("os.name");
return osName.toLowerCase(Locale.ROOT).contains("win");
}
}
public class LinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
String osName = conditionContext.getEnvironment().getProperty("os.name");
return osName.toLowerCase(Locale.ROOT).contains("linux");
}
}
写一个JavaConfig,根据环境选择注入的Bean
@Configuration
public class JavaConfig {
//@Bean取一个相同的名字,用哪个取决于当前环境
//@Conditional判断环境,返回true则注入
@Bean("cmd")
@Conditional(WindowsCondition.class)
ShowCmd winCmd(){
return new WindowsShowCmd();
}
@Bean("cmd")
@Conditional(LinuxCondition.class)
ShowCmd linuxCmd(){
return new LinuxShowCmd();
}
}
主方法加载配置文件得到ctx并调用getBean构造对象,运行方法。
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
ShowCmd cmd = (ShowCmd) ctx.getBean("cmd");
String s = cmd.showCmd();
System.out.println(s);
}
}
7.2 多环境切换
在开发中,要进行 开发/生产/测试 环境之间的切换,Spring提供了Profile来实现,其底层是条件注解。
我们定义一个 DataSource:
public class DataSource {
private String url;
private String username;
private String password;
}
然后,在配置 Bean 时,通过 @Profile 注解指定不同的环境:
@Bean("ds")
@Profile("dev")
DataSource devDataSource() {
DataSource dataSource = new DataSource();
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/dev");//本机环境
dataSource.setUsername("root");
dataSource.setPassword("1234");
return dataSource;
}
@Bean("ds")
@Profile("prod")
DataSource prodDataSource() {
DataSource dataSource = new DataSource();
dataSource.setUrl("jdbc:mysql://183.158.222.33:3306/dev");//服务器环境
dataSource.setUsername("baihui");
dataSource.setPassword("123456");
return dataSource;
}
最后,在加载配置类,注意,需要先设置当前环境,然后再去加载配置类:
public class JavaMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("dev");
ctx.register(JavaConfig.class);
ctx.refresh();
DataSource ds = (DataSource) ctx.getBean("ds");
System.out.println(ds);
}
}
这个是在 Java 代码中配置的。环境的切换,也可以在 XML 文件中配置,如下配置在 XML 文件中,必须放在其他节点后面。
<beans profile="dev">
<bean class="org.javaboy.DataSource" id="dataSource">
<property name="url" value="jdbc:mysql:///devdb"/>
<property name="password" value="root"/>
<property name="username" value="root"/>
</bean>
</beans>
<beans profile="prod">
<bean class="org.javaboy.DataSource" id="dataSource">
<property name="url" value="jdbc:mysql://111.111.111.111/devdb"/>
<property name="password" value="jsdfaklfj789345fjsd"/>
<property name="username" value="root"/>
</bean>
</beans>
启动类中设置当前环境并加载配置:
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();
ctx.getEnvironment().setActiveProfiles("prod");
ctx.setConfigLocation("applicationContext.xml");
ctx.refresh();
DataSource dataSource = (DataSource) ctx.getBean("dataSource");
System.out.println(dataSource);
}
}
8. 其他
Bean的作用域
从 Spring 容器中多次获取同一个 Bean,默认情况下,获取到的实际上是同一个实例(满足日常需求)。若需求是获取不同实例,当然我们可以自己手动配置。
进行如下配置,scope=“prototype”,使每次获取到的不是同一个实例:
<bean class="org.javaboy.User" id="user" scope="prototype" />
在java配置中也可以使用@Scope(“prototype”)指定多次获取不是同一个实例。
id和name的区别
没什么太大的区别,只是name后面加逗号可以表示多个对象的名称,而id后面只能表示一个。
如下配置,通过 user、user1、user2、user3 都可以获取到当前对象:
<bean class="org.javaboy.User" name="user,user1,user2,user3" scope="prototype"/>
混合配置
混合配置就是 Java 配置+XML 配置。混用的话,可以在 Java 配置中引入 XML 配置。
@Configuration
@ImportResource("classpath:applicationContext.xml")
public class JavaConfig {
}
在 Java 配置中,通过 @ImportResource 注解可以导入一个 XML 配置。
先鸽了未完待续