技术学习:黑马程序员 Java Spring

学习自:黑马程序员 Java Web 课程 + JavaBooks: Java 程序员必读书单(超 1000 本 PDF,附下载地址)

介绍

Spring:分层的 Java SE/EE 全栈轻量级开源框架,以 控制反转(Inverse of Control IOC)和面向切面编程(Aspect Orient Programming AOP)为内核。

提供了展现层 SpringMVC 和持久层 Spring JDBCTemplate 以及业务层事务管理等众多企业级应用技术,是使用最多的 Java EE 企业应用开源框架。

优势:

  • 方便解耦,简化开发通过 Spring 提供的 loC 容器,可以将对象间的依赖关系交由 Spring 进行控制,避免硬编码所造成的过度耦合。用户也不必再为单例模式类、属性文件解析等这些很底层的需求编写代码,可以更专注于上层的应用。
  • AOP 编程的支持通过 Spring 的 AOP 功能,方便进行面向切面编程,许多不容易用传统 OOP 实现的功能可以通过 AOP 轻松实现。
  • 声明式事务的支持,让事务管理更简单。
  • 方便调试。
  • 方便集成框架。
  • 降低 Java EE API 使用难度(如 JDBC,远程调用等)。
  • 其源码是经典学习范例。

IOC

控制反转(Inversion Of Control IOC):对象的创建控制权不是在程序中,而是转移到外部(容器)。类似线程池吧,不是我们自己手动创建对象而是容器帮我们创建对象,要使用的时候从容器里面拿。

Bean 对象:IOC 容器中创建,管理的对象。比如上面的 A 对象就是 bean。

在大多数应用场景中,不需要明确的用户代码来实例化 Spring IoC 容器的一个或多个实例。例如,在 Web 应用场景中,通常只需在应用程序的 web.xml 文件中编写 8 行(或更多)模板式的 Web 描述符就足够了.

DL

依赖查找(Dependency Lookup):注册到 spring 容器中的依赖,通过类似 getBean 这样的方式查找获取出来。查找名称或者 id。

DI

依赖注入(Dependency Injection):组装器自动注入,大致比如我容器里有一个 Student 对象,然后我声明了一个 Student 对象并且让其从容器里面去找,那么 spring 就会自动将容器中的 spring 分配给我指定要依赖注入的对象。

Note

注册是 Bean 的定义过程,注入是 Bean 的依赖解析过程.

注册发生在容器启动阶段(如解析 XML/注解).

注入发生在 Bean 实例化阶段(依赖关系装配).

Maven

Apache 下一个开源 Java 项目,用于管理,构建 Java 项目的工具。

Note

Apache 是最大的开源软件基金会。

  1. 方便快捷地管理项目依赖(jar 包),避免冲突问题。手动导入 jar 包,一个项目可能会导入上百个,彼此之间还有相互依赖,一个升级了会影响很多也要一起升级才能正常工作。maven 只需要在 pom.xml 文件里面寥寥几句就能定义要引入的 jar 包。
  2. 提供统一的项目结构。不同 IDE 构建生成的项目(如 eclipse, IDEA)彼此之间不通用,而 maven 提供了一套标准项目结构。
  3. 标准跨平台的自动化项目构建方式。不同平台构建项目方式可能也不同,maven 则提供了一套标准,统一的项目结构,编译后放到 target 下,还可快捷打包。2,3 如下图。

main:实际项目资源。

Java:源代码目录。

resource:配置文件目录。

test:测试项目资源。

pom.xml:项目配置文件。

Note

pom 全称是 Project Object Model 项目对象模型。

pom 是在 pom.xml 中的一小段信息用于描述该项目。包括 组织名(groupId,通常是域名反写),模块名(项目名称,通常是模块名称,比如 order-service),版本号(这三个合起来被称作 maven 的坐标,可以唯一定义项目,或引入依赖)。

dependency 就是 pom.xml 中添加的依赖,maven 会自动去我们电脑的仓库中找对应的依赖包(本地仓库)。

中央仓库是全球唯一的,maven 核心团队运营的,几乎包含了全世界发布的所有 jar 包。

远程仓库(私服)是公司团队搭建的私有仓库。

优先查找本地 -> 私服 -> 中央仓库,只要有一个人用到一个新 jar 包,这个包就会从中央仓库下载到私服。

安装我也不赘述了,网上好教程很多。

引入其他项目的 maven 就是在 IDEA 中导入其 pom.xml 文件。

依赖配置

在 pom.xml 中写 dependencies 依赖,在其中写 dependency 单个依赖,并声明要引入的包的坐标。

然后需要点击刷新才能引入。

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.13</version>
</dependency>
</dependencies>

可以去 mvnrepository.com 中找要引入的包,按下载量选择版本号比较好。

依赖传递

上面这个依赖其实需要其他两个依赖。不过点击刷新的时候自动全部引入了。

直接依赖

我们通过依赖配置引入的依赖。

间接依赖

直接依赖项对其他依赖的依赖。

可以在 IDEA 中以图表形式查看依赖。

排除依赖

比如 A 依赖了 B,B 依赖了 C,但是 A 不想依赖 C:

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.13</version>
</dependency>
<exclusion>
<exclusion>
<groupId></groupId> <!-- 在这里写不想依赖的包的组织名和模块名 -->
<artifactId></artifactId>
</exclusion>
</exclusion>
</dependencies>

依赖范围

可以限定 jar 包在主程序范围内是否生效(main 文件夹),测试程序中是否也生效(test 文件夹),是否参与打包运行(package 指令)。

通过 scope 标签声明。

1
2
3
4
5
6
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.13</version>
</dependency>
<scope>test</scope>
scope 值 主程序 测试程序 打包(运行) 范例
compile(默认) Y Y Y log4j
test - Y - junit
provided Y - - servlet-api
runtime - Y Y jdbc 驱动

在 IDEA - maven 列表 - 生命周期 - package 双击可以进行打包测试看设置不同 scope 值的 jar 包是否被引入了。

生命周期

maven 的生命周期就是为了对所有 maven 项目的构建进行统一。

Maven 中有 3 套相互独立的生命周期:

  • clean:清理工作。
  • default:核心工作,如:编译、测试、打包、安装、部署等。
  • site:生成报告、发布站点等。
Note

在同一套生命周期中,运行后面的工作前面也会运行,比如运行打包,编译测试也会运行。不过 clean 不会运行因为不属于同一套生命周期。

IDEA 中双击对应的生命周期项目就能执行,点击上面这个符号可以跳过测试。

也可以终端 mvn compile 这样执行。

Note

其实真正执行这些命令的时候,生命周期是依赖下面这些插件来具体执行的。

Spring 程序开发步骤

  1. 导入 Spring 开发的基本包坐标。
  2. 编写要注入的接口和实现类。
  3. 创建 Spring 核心配置文件,并在其中注册 bean。
  4. 使用 Spring API 获得 Bean 实例。

引入 Spring

在 IDEA 中新建一个 Maven 项目,在 pom.xml 中引入 springframework 依赖:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
</dependencies>

这样就算引入 Spring 了。

Bean 手动注册

XML 注册

属性注册

  1. 构造方法注册方式:首先给对应的 Bean 添加构造方式(假设这里添加的是 Book(Integer id, String name, Double price),然后在 xml 配置中:

    1
    2
    3
    4
    5
    <bean class="io.github.Book" id="book">
    <constructor-arg index="0" value="1" />
    <constructor-arg index="1" value="三国演义" />
    <constructor-arg index="2" value="30" />
    </bean>

    三个 index 对应三个构造方法中的参数。这三条顺序可以颠倒,但是 index 和 value 要按照构造方法中的顺序一一对应。

  2. 构造方法指定参数名注册:可读性更高,适用于参数数量较多以及语义性好的要求场景下。且重构性好,比如构造方法中的参数顺序变了,1 方法就会出错,这个方法受顺序影响并不大。

    1
    2
    3
    4
    5
    <bean class="io.github.Book" id="book">
    <constructor-arg name="id" value="1" />
    <constructor-arg name="name" value="三国演义" />
    <constructor-arg name="price" value="30" />
    </bean>
  3. set 方法注册。

    1
    2
    3
    <bean class="io.github.Book" id="book">
    <property name="id" value="3" />
    </bean>
    Note

    这里的属性名不是自己定义的 bean 属性名,而是 java 通过内省机制分析出来的属性名。

  4. p 名称空间注册:本质也是利用了 set 方法。<bean class="io.github.Book" id="book" p:id="4" p:bookName="西游记" p:price="33"></bean>

外部 Bean 注册

有的时候使用一些 Bean 不是通过构造方法 new 出来的,比如工厂模式使用 Builder 外部方法创建的,这种时候就不能用之前的构造方法属性注册了。

静态工厂注册:可以首先单独写一个 OkHttpUtils.getInstance() 获取静态工厂的静态方法(其内部也是调用 Builder 方法),然后在 xml 中配置该静态工厂:

1
<bean class="org.javaboy.OkHttpUtils" factory-method="getInstance" id="okHttpClient"></bean>

相当于:调用 OkHttpUtils.getInstance() 方法,获取 okHttpClient 实例对象。

实例工厂注册:getInstance 是非静态方法,所以要先创建 okHttpUtils 对象才能调用工厂方法。

1
2
3
4
<bean class="org.javaboy.OkHttpUtils" id="okHttpUtils" />
<bean id="okHttpClient"
factory-bean="okHttpUtils" <!-- 指向工厂Bean -->
factory-method="getInstance"/> <!-- 工厂方法 -->

自己写的 Bean 一般不会用这两种注册方法,但是引入外部 jar 包的时候可能会用到。

复杂属性注册

对象注册

可以通过 ref 引入一个对象。

1
2
3
4
5
6
<bean class="org.javaboy.User" id="user">
<property name="cat" ref="cat"/>
</bean>
<bean class="org.javaboy.Cat" id="cat"><property name="name" value="小白"/>
<property name="color" value="白色"/>
</bean>
数组注册

array list 用法一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<bean class="org.javaboy.User" id="user">
<property name="cat" ref="cat"/>
<property name="favorites">
<list>
<value>足球</value>
<value>篮球</value>
<value>乒乓球</value>
</list>
</property>
<property name="cats">
<list>
<ref bean="cat"/>
<ref bean="cat2"/>
<bean class="org.javaboy.Cat" id="cat3">
<property name="name" value="小花"/>
<property name="color" value="花色"/>
</bean>
</list>
</property>
</bean>
map 注册
1
2
3
4
5
<property name="map">
<map>
<entry key="age" value="99"/><entry key="name" value="javaboy"/>
</map>
</property>
Properties 注册
1
2
3
4
5
6
<property name="info">
<props>
<prop key="age">99</prop>
<prop key="name">javaboy</prop>
</props>
</property>

Java 配置注册

java 配置注册本来用的很少。在 sprintboot 中用的很多,因为 springboot 中不使用一行 xml 配置,所以就不能使用之前的 applicationContext.xml 方法。

1
2
3
4
5
6
7
@Configuration
public class JavaConfig {
@Bean
SayHello sayHello(){
return new SayHello();
}
}
  1. 配置类有一个 @Configuration 注解,表示这个类不是普通类,是一个配置类,相当于 applicationContext.xml.
  2. 定义方法和方法返回对象,方法添加 @Bean 注解,表示方法的返回值注册 Spring 容器中。这个方法相当于 applicationContext.xml 中的 bean 节点。

Bean DL 查找注入

XML 配置 DL

比如我有一个 io.github.Book 类,然后想在 io.github.Main 中使用但不直接创建对象而是用 IOC 的方式使用:

  1. resource/applicationContext.xml 中配置 Book 类:

    1
    2
    3
    4
    5
    6
    7
    8
    <?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="io.github.Book" id="book" />
    </beans>
  2. 在 Main 中引入:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import org.springframework.context.support.ClassPathXmlApplicationContext;

    public class Main {
    public static void main(String[] args) {
    ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");// 加载 resource xml 文件
    Book book1 = (Book)ctx.getBean("book"); // 通过名称获取 bean
    System.out.println(book);
    // 也可以通过 class 获取,但是如果有多个同名不同 id 的 bean,这种方法就不行,所以一般建议还是用 id 和 name 获取
    Book book2 = ctx.getBean(Book.class);
    }
    }

Java 配置 DL

获取 bean:主要区别就是原先是加载 xml 类,现在是加载配置类。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args){
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
SayHello hello = ctx.getBean(SayHello.class);
System.out.println(hellp.sayHello("javaboy"));
}
}

Bean 名默认是方法名,可以主动声明:@Bean("javaboy").

自动化配置注册

可以通过一些注解,标注哪些类想要被注册到 spring 容器中。这样的注解主要有四个:@Repository(用于数据层),@Service(用于服务层),@Controller(用于控制层),@Component(用于其他类)。

添加完成之后,自动化扫描也主要有两种方式:Java 配置和 xml。

Java 配置的自动化注册

1
2
3
4
5
6
@Configuration
@ComponentScan(basePackages = "org.javaboy.javaconfig.service")// 自动扫描注册这个包里面的 bean,比如这个包里有一个 @Service 的 UserService 类,就会被扫描并注册。不写默认扫描当前包和子包
// 还可以通过一些 Filter 按类型选择性筛选要注册的包
public class JavaConfig {
// 里面包含了一些需要手动注册的类,比如第三方库,复杂初始化逻辑的类,或者无法添加 @Component 注解的类
}

然后获取 Bean 的注入方法还是 java 配置 DL 的方法,AnnotationConfigApplicationContext 类。

Bean 名字:默认类名首字母小写,也可以自己指定,比如 @Service("userService")

XML 配置的自动化注册

1
<context:component-scan base-package="org.javaboy.javaconfig" />

Bean DI 自动化配置注入

  1. @Autowired:按类型查找。要求容器中只能有一个对应类型的 bean 否则会报错。结合 @Qualifier 指定变量名的话可以打破这个限制。

    1
    2
    3
    4
    @Autowired
    @Qualifier("cat") // 可以直接声明对应类名小写,这样 Spring 就知道我们选的是哪个类。
    // 如果想自定义其他类名,需要在 @Component 里也声明对应的 value,如@Component("objectName")
    private Animal animal;
  2. @Resources:按变量名查找。

    1
    2
    3
    @Resource(name = "cat")	// 不用写 Autowired 了。Resource 是 jdk 中的注解不是 Spring 中的。
    // Autowired 默认是按照类型进行注入的,而 Resource 默认是按照名称注入的
    private Animal animal;
  3. @Inject

条件注解

比如我想写一个执行终端命令的类,这个类要依平台而定,windows 平台执行 dir 命令而 linux 平台执行 ls 命令。

首先我们创建一个 ShowCmd 接口,里面有 return String 的 showCmd() 待实现方法。然后创建 WinShowCmdLinuxShowCmd 类分别重写实现这个接口。

然后创建两个 Condition 类,用于判断系统是 Win 还是 Linux:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class WindowsCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().getProperty("os.name").toLowerCase().contains("windows");
}
}

public class LinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().getProperty("os.name").toLowerCase().contains("linux");
}
}

然后条件注解使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class JavaScript {
@Bean("showCmd")
@Conditional(WindowsCondition.class)
ShowCmd winCmd() {
return new WinShowCmd();
}

@Bean("showCmd")
@Conditional(LinuxCondition.class)
ShowCmd linuxCmd() {
return new LinuxShowCmd();
}
}

注意这里,我们给两个 Bean 创建了相同的名字进行注册。这样 getBean("showCmd") 的时候就可以选择性获取到自己系统的 Bean 了。

多环境切换

条件注解的典型使用场景就是多环境切换,这里不是说操作系统的环境或者环境变量,而是说生产/开发/测试环境。

Spring 中提供了 @Profile 这个注解来解决多环境切换问题,底层就是条件注入实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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("123");
return dataSource;
}

@Bean("ds")
@Profile("prod")
DataSource prodDataSource() {
DataSource dataSource = new DataSource();
dataSource.setUrl("jdbc:mysql://192.158.222.33:3306/dev");
dataSource.setUsername("jkldasjfkl");
dataSource.setPassword("jfsdjflkajkld");
return dataSource;
}

使用的时候:记得先设定当前环境再按名称条件查找。

1
2
3
4
5
6
7
8
9
10
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);
}
}

XML 中类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 开发环境配置 -->
<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>

其他

Bean 的作用域

默认 Bean 随着容器创建而创建,随着容器销毁而销毁。所以多次从容器中获取 Bean,获取到的都是同一对象。

也可以手动配置其作用域。在 xml 中:<bean class="..." scope="prototype"> 默认是 singleton,prototype 的话每次获取会获得不同的实例。还有 request 和 session,在 web 环境中生效。

或者在 Java 配置中(自动扫描中也类似):

1
2
3
4
5
6
7
public class JavaConfig {
@Bean
@Scope("prototype")
SayHello sayHello(){
// ...
}
}

id 和 name 的区别

大多数时候差不多。有一点小区别,一个 bean 可以指定多个 name,但是只有一个 id:

1
<bean class="..." name="user1,user2,user3" />

上面这个例子通过 user1, user2, user3 这三个 name 都可以获取到同一个实例。但是如果换成 id,上面这个用法就相当于只注册了一个 id 为 “user1, user2, user3” 的 bean。

混合配置

Java 配置+ XML。java 中通过 @ImportResource 引入 xml 配置。

1
2
3
4
5
@Configuration
@ImportResource("classpath:applicationContext.xml")
public class JavaConfig {
// 配置类内容
}

Aware 接口

Spring 容器里面的类就是通过 Aware 被获取到其内部信息的。我们自己也可以调用一些 Aware 类获取容器内部信息,比如 bean 名称,BeanFactory,ApplicationContext 等。

AOP

面向切面编程(Aspect Oriented Programming):程序运行的时候,不改变程序源码的前提下,动态增强方法的功能。

  • 日志
  • 事务
  • 数据库操作

可以一定程度上去除很多重复的模板化代码。

概念 说明
切点 要添加增强代码的目标位置(如特定方法)
通知(增强) 向切点动态添加的增强代码(如日志、事务等逻辑)
包括:
前置通知
后置通知
异常通知
返回通知
环绕通知
切面 切点 + 通知的组合(AOP 的核心抽象单元)
连接点 程序执行过程中可插入切面的点(如方法调用、异常抛出等具体执行位置)

Spring AOP 开发流程:

  1. 定义普通业务组件。
  2. 定义切入点。一个切入点可能横跨多个业务组件。
  3. 定义增强组件,就是 AOP 框架给普通组件织入的处理动作。

引入 AOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>org.springframework</groupId> <!-- 修正闭合标签 -->
<artifactId>spring-context</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
</dependencies>

定义切点

主要有两种方法:自定义注解和使用规则。

自定义注解(名叫Action)的示例:

1
2
3
4
5
@Target(ElementType.METHOD)  // 指定注解只能用于方法上
@Retention(RetentionPolicy.RUNTIME) // 指定注解在运行时保留(可通过反射读取)
public @interface Action { // 定义一个名为Action的注解
// 没有定义属性,这是一个标记注解
}

然后在要拦截的方法处添加 Action 注解,该方法就会被 AOP 拦截,其他方法不受影响:

1
2
3
4
5
6
7
8
9
10
11
@Component
public class MyCalculatorImpl {
@Action
public int add(int a, int b) {
return a + b;
}

public void min(int a, int b) {
System.out.println(a + "-" + b + "=" + (a - b));
}
}
Contact Me
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022-2025 Jingqing3948

星光不问,梦终有回

LinkedIn
公众号