Spring 有两个核心部分:IoC 和AOP

  • IoC:控制反转,把创建对象过程交给 Spring 进行管理
  • AOP:面向切面,不修改源代码进行功能增强

IoC

IoC全称Inversion of Control,直译为控制反转,把对象创建和对象之间的调用过程,交给 Spring 进行管理 ,又称为依赖注入(DI:Dependency Injection),它解决了一个最主要的问题:将组件的创建+配置与组件的使用相分离,并且,由IoC容器负责管理组件的生命周期。

Xml方式

简单例子:

1
2
3
4
5
public class User {
public void show() {
System.out.println("------show------");
}
}

spring config 的 bean1.xml:

1
2
3
4
5
6
7
8
9
<?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">

<!--配置User对象创建-->
<bean id="user" class="com.kanxz.spring5.User"/>

</beans>

最后测试代码:

1
2
3
4
5
6
7
8
@Test
public void testUser() {
// 加载spring配置文件
ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
// 获取配置创建的对象
User user = context.getBean(User.class);
user.show();
}

基于 xml 方式创建对象

maven项目目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
|-- pom.xml
`-- src
|-- main
| |-- java
| | `-- com
| | `-- kanxz
| | `-- spring5
| | `-- Book.java
| `-- resources
| `-- bean1.xml
`-- test
`-- java

bean1.xml为spring config文件

创建对象:

1
<bean id="book" class="com.kanxz.spring5.Book"/>
  • 在 spring 配置文件中,使用 bean 标签,标签里面添加对应属性,就可以实现对象创建

  • 在 bean 标签有很多属性,介绍常用的属性

    • id 属性:唯一标识
    • class 属性:类全路径(包类路径)
  • 创建对象时候,默认也是执行无参数构造方法完成对象创建

基于 xml 方式注入属性

第一种注入方式:使用 set 方法进行注入

创建Book类,有属性bookNamebookAuthor,并定义set方法。

在 spring 配置文件(bean1.xml)配置对象创建,配置属性注入

1
2
3
4
5
6
7
8
9
<!--set方法注入属性--> 
<bean id="book" class="com.kanxz.spring5.Book">
<!--使用property完成属性注入
name:类里面属性名称
value:向属性注入的值
-->
<property name="bookName" value="C++"/>
<property name="bookAuthor" value="Java"/>
</bean>
第二种注入方式:使用有参数构造进行注入

定义有含参构造方法的类

在 spring 配置文件中进行配置:

1
2
3
4
5
<!--有参构造注入属性-->
<bean id="order" class="com.kanxz.spring5.Order">
<constructor-arg name="orderName" value="电脑"/>
<constructor-arg name="orderAddress" value="China"/>
</bean>
p 名称空间注入

使用 p 名称空间注入,可以简化基于 xml 配置方式

第一步 添加 p 名称空间在配置文件中

1
2
3
4
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

第二步 进行属性注入,在 bean 标签里面进行操作

1
2
<!--p名称空间注入: set注入-->
<bean id="book1" class="com.kanxz.spring5.Book" p:bookName="C++" p:bookAuthor="李四"/>
xml 注入其他类型属性
1
2
3
4
5
6
7
8
9
10
11
12
13
<bean id="book" class="com.kanxz.spring5.Book">
<!--包含特殊符号:<<Java>>
1. 使用转义:&lt; &gt;
2. 把带特殊字符的内容写到CDATA-->
<property name="bookName">
<value><![CDATA[<<Java>>]]></value>
</property>

<!--空值的设置-->
<property name="bookAddress">
<null/>
</property>
</bean>
注入属性-外部 bean

(1)创建两个类 service 类和 dao 类
(2)在 service 调用 dao 里面的方法
(3)在 spring 配置文件中进行配置

1
2
3
4
5
6
7
<bean id="userService" class="com.kanxz.spring5.service.UserService">
<!--注入UserDao对象
name: 类里面的属性名称
ref: 创建UserDao对象bean标签的id值-->
<property name="userDao" ref="userDaoImpl"/>
</bean>
<bean id="userDaoImpl" class="com.kanxz.spring5.dao.UserDaoImpl"/>
注入属性-内部 bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Dept { 
private String dname;
public void setDname(String dname) {
this.dname = dname;
}
}

public class Emp {
private String ename;
private String gender;
private Dept dept;
public void setDept(Dept dept) {
this.dept = dept;
}
public void setEname(String ename) {
this.ename = ename;
}
public void setGender(String gender) {
this.gender = gender;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
<!--内部bean--> 
<bean id="emp" class="com.kanxz.spring5.bean.Emp">
<!--设置两个普通属性-->
<property name="ename" value="lucy"></property>
<property name="gender" value="女"></property>
<!--设置对象类型属性-->
<property name="dept">
<bean id="dept" class="com.kanxz.spring5.bean.Dept">
<property name="dname" value="安保部"></property>
</bean>
</property>
</bean>
注入属性-级联赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
<!--级联赋值-->
<bean id="emp" class="com.kanxz.spring5.bean.Emp">
<property name="empName" value="李四"/>
<property name="empGender" value="女"/>
<property name="dept" ref="dept"/>
<!--方式二-->
<property name="dept.deptName" value="财务部"/>
</bean>

<bean id="dept" class="com.kanxz.spring5.bean.Dept">
<!--方式一-->
<property name="deptName" value="技术部"/>
</bean>
xml 注入集合属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!--集合类型属性注入--> 
<bean id="stu" class="com.kanxz.spring5.collectiontype.Stu">
<!--数组类型属性注入-->
<property name="courses">
<array>
<value>java课程</value>
<value>数据库课程</value>
</array>
</property>
<!--list类型属性注入-->
<property name="list">
<list>
<value>张三</value>
<value>小三</value>
</list>
</property>
<!--map类型属性注入-->
<property name="maps">
<map>
<entry key="JAVA" value="java"></entry>
<entry key="PHP" value="php"></entry>
</map>
</property>
<!--set类型属性注入-->
<property name="sets">
<set>
<value>MySQL</value>
<value>Redis</value>
</set>
</property>
</bean>
在集合里面设置对象类型值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--创建多个course 对象--> 
<bean id="course1" class="com.kanxz.spring5.collectiontype.Course">
<property name="cname" value="Spring5 框架"></property>
</bean>
<bean id="course2" class="com.kanxz.spring5.collectiontype.Course">
<property name="cname" value="MyBatis 框架"></property>
</bean>
<!--注入list集合类型,值是对象-->
<property name="courseList">
<list>
<ref bean="course1"></ref>
<ref bean="course2"></ref>
</list>
</property>

基于注解方式

1、什么是注解
(1)注解是代码特殊标记,格式:@注解名称(属性名称=属性值, 属性名称=属性值…)
(2)使用注解,注解作用在类上面,方法上面,属性上面
(3)使用注解目的:简化 xml 配置

2、Spring 针对 Bean 管理中创建对象提供注解
(1)@Component
(2)@Service
(3)@Controller
(4)@Repository

上面四个注解功能是一样的,都可以用来创建bean 实例

基于注解方式实现对象创建

1
2
3
4
5
6
7
8
|-- com
`-- kanxz
`-- spring5
|-- dao
| |-- UserDao.java
| `-- UserDaoImpl.java
`-- service
`-- UserService.java

开启组件扫描:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
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/context http://www.springframework.org/schema/context/spring-context.xsd">

<!--开启组件扫描
1 如果扫描多个包,多个包使用逗号隔开
2 扫描包上层目录-->
<context:component-scan base-package="com.kanxz.spring5"/>
</beans>

创建类,在类上面添加创建对象注解:

1
2
3
4
5
6
@Component(value = "userService")  //<bean id="userService" class=".."/> 
public class UserService {
public void add() {
System.out.println("service add.......");
}
}

基于注解方式实现属性注入

@Autowired:根据属性类型进行自动装配

第一步 把 service 和 dao 对象创建,在 service 和 dao 类添加创建对象注解
第二步 在 service 注入 dao 对象,在 service 类添加 dao 类型属性,在属性上面使用注解

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service 
public class UserService {
//定义dao类型属性
//不需要添加set 方法
//添加注入属性注解
@Autowired
private UserDao userDao;

public void add() {
System.out.println("service add.......");
userDao.add();
}
}
@Qualifier:根据名称进行注入

这个@Qualifier 注解的使用,和上面@Autowired 一起使用

1
2
3
4
5
6
//定义dao类型属性 
//不需要添加set方法
//添加注入属性注解
@Autowired //根据类型进行注入
@Qualifier(value = "userDaoImpl") //根据名称进行注入
private UserDao userDao;
@Resource:可以根据类型注入,可以根据名称注入
1
2
3
//@Resource  //根据类型进行注入 
@Resource(name = "userDaoImpl1") //根据名称进行注入
private UserDao userDao;
@Value:注入普通类型属性
1
2
@Value(value = "abc") 
private String name;

完全注解方式

1
2
3
4
5
6
7
8
9
|-- com
`-- kanxz
`-- spring5
`-- AppConfig.java
|-- dao
| |-- UserDao.java
| `-- UserDaoImpl.java
`-- service
`-- UserService.java

创建配置类,替代xml配置文件

1
2
3
4
5
@Configuration
@ComponentScan
public class AppConfig {
...
}

AppConfig标注了@Configuration,表示它是一个配置类

AppConfig标注了@ComponentScan,它告诉容器,自动搜索当前类所在的包以及子包,把所有标注为@Component的Bean自动创建出来,并根据@Autowired进行装配。

编写测试类

1
2
3
4
5
6
7
8
@Test 
public void testService2() {
//加载配置类
ApplicationContext context = new AnnotationConfigApplicationContext(APPConfig.class);
UserService userService = context.getBean(UserService.class);
System.out.println(userService);
userService.add();
}

使用Annotation配合自动扫描能大幅简化Spring的配置,我们只需要保证:

  • 每个Bean被标注为@Component并正确使用@Autowired注入;
  • 配置类被标注为@Configuration@ComponentScan
  • 所有Bean均在指定包以及子包内。

使用@ComponentScan非常方便,但是,我们也要特别注意包的层次结构。通常来说,启动配置AppConfig位于自定义的顶层包,其他Bean按类别放入子包。

定制Bean

使用Resource

在Java程序中,我们经常会读取配置文件、资源文件等。使用Spring容器时,我们也可以把“文件”注入进来,方便程序读取。

例如,AppService需要读取logo.txt这个文件,通常情况下,我们需要写很多繁琐的代码,主要是为了定位文件,打开InputStream。

Spring提供了一个org.springframework.core.io.Resource(注意不是javax.annotation.Resource),它可以像Stringint一样使用@Value注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class AppService {
@Value("classpath:/logo.txt")
private Resource resource;

private String logo;

@PostConstruct
public void init() throws IOException {
try (var reader = new BufferedReader(
new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
this.logo = reader.lines().collect(Collectors.joining("\n"));
}
}
}

注入Resource最常用的方式是通过classpath,即类似classpath:/logo.txt表示在classpath中搜索logo.txt文件,然后,我们直接调用Resource.getInputStream()就可以获取到输入流,避免了自己搜索文件的代码。

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
|-- pom.xml
`-- src
|-- main
| |-- java
| | `-- com
| | `-- kanxz
| | `-- spring5
| | |-- AppConfig.java
| | `-- AppService.java
| `-- resources
| `-- logo.txt
`-- test
`-- java

注入配置

在开发应用程序时,经常需要读取配置文件。最常用的配置方法是以key=value的形式写在.properties文件中。

要读取配置文件,我们可以使用Resource来读取位于classpath下的一个app.properties文件。但是,这样仍然比较繁琐。

Spring容器还提供了一个更简单的@PropertySource来自动读取配置文件。我们只需要在@Configuration配置类上再添加一个注解:

1
2
3
4
5
6
7
8
9
10
11
@ComponentScan
@Configuration
@PropertySource("/app.properties") // 表示读取resources的app.properties
public class AppConfig {

@Value("${app.username}")
private String username;

@Value("${app.password}")
private String password;
}
1
2
app.username=Tom
app.password=123456

Spring容器看到@PropertySource("/app.properties")注解后,自动读取这个配置文件,然后,我们使用@Value正常注入。

注意注入的字符串语法,它的格式如下:

  • "${app.username}"表示读取key为app.username的value,如果key不存在,启动将报错;
  • "${app.username:Mary}"表示读取key为app.username的value,但如果key不存在,就使用默认值Mary

另一种注入配置的方式是先通过一个简单的JavaBean持有所有的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ComponentScan
@Configuration
@PropertySource("/app.properties")
public class AppConfig {

@Value("${app.username}")
private String username;

@Value("${app.password}")
private String password;

public String getUsername() {
return username;
}

public String getPassword() {
return password;
}
}

然后,在需要读取的地方,使用#{appConfig.username}注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class AppService {

@Value("#{appConfig.username}")
private String username;

@Value("#{appConfig.password}")
private String password;

public void show() {
System.out.println(username);
System.out.println(password);
}

}

注意观察#{}这种注入语法,它和${key}不同的是,#{}表示从JavaBean读取属性。"#{appConfig.username}"的意思是,从名称为appConfig的Bean读取username属性,即调用getUsername()方法。一个Class名为AppConfig的Bean,它在Spring容器中的默认名称就是appConfig,除非用@Qualifier指定了名称。

使用一个独立的JavaBean持有所有属性,然后在其他Bean中以#{bean.property}注入的好处是,多个Bean都可以引用同一个Bean的某个属性。例如,如果AppConfig决定从数据库中读取相关配置项,那么AppService注入的@Value("#{appConfig.username}")仍然可以不修改正常运行。

目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
`-- src
|-- main
| |-- java
| | `-- com
| | `-- kanxz
| | `-- spring5
| | |-- AppConfig.java
| | `-- AppService.java
| `-- resources
| `-- app.properties
`-- test
`-- java
`-- AppTest.java

AOP

AOP是Aspect Oriented Programming,即面向切面编程。

利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

通俗描述:不通过修改源代码方式,在主干功能里面添加新功能

AOP 底层使用动态代理,有两种情况动态代理

  • 第一种 有接口情况,使用 JDK 动态代理,创建接口实现类代理对象,增强类的方法
  • 第二种 没有接口情况,使用 CGLIB 动态代理,创建子类的代理对象,增强类的方法

在AOP编程中,我们经常会遇到下面的概念:

  • Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点;
  • Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行;
  • Pointcut:切入点,即一组连接点的集合;
  • Advice:通知(增强),指特定连接点上执行的动作;
    • 前置通知(Before):在目标方法被调用之前调用通知功能。
    • 后置通知(After):在目标方法完成之后调用通知,无论该方法是否发生异常。
    • 后置返回通知(After-returning):在目标方法成功执行之后调用通知。
    • 后置异常通知(After-throwing):在目标方法抛出异常后调用通知。
    • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
  • Introduction:引介,指为一个已有的Java对象动态地增加新的接口;
  • Weaving:织入,指将切面整合到程序的执行流程中;
  • Interceptor:拦截器,是一种实现增强的方式;
  • Target Object:目标对象,即真正执行业务的核心逻辑对象;
  • AOP Proxy:AOP代理,是客户端持有的增强后的对象引用。

装配AOP

看一个例子来更快了解:

首先,我们通过Maven引入Spring对AOP的支持:

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.12.RELEASE</version>
</dependency>

上述依赖会自动引入AspectJ,使用AspectJ实现AOP比较方便,因为它的定义比较简单。

基本目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|-- pom.xml
`-- src
|-- main
| |-- java
| | `-- com
| | `-- kanxz
| | `-- spring5
| | |-- AppConfig.java
| | |-- LoginAspect.java
| | `-- service
| | |-- MailService.java
| | |-- User.java
| | `-- UserService.java
| `-- resources
`-- test
`-- java

其中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class MailService {

public String getTime() {
return LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}

public void sendLoginMail(User user) {
System.err.printf("Hi, %s! You are login at %s\n", user.getName(), getTime());
}

public void sendRegistrationMail(User user) {
System.err.printf("Hello %s!\n", user.getName());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Component
public class UserService {

private final MailService mailService;

public UserService(MailService mailService) {
this.mailService = mailService;
}

private List<User> users = new ArrayList<>(List.of(
new User(1, "Tom@example.com", "pd", "Tom"),
new User(2, "Bob@example.com", "pd", "Bob"),
new User(3, "Alice@example.com", "pd", "Alice")
));

public User login(String email, String pd) {
for (User user : users) {
if (user.getEmail().equalsIgnoreCase(email) && user.getPassword().equalsIgnoreCase(pd)) {
mailService.sendLoginMail(user);
return user;
}
}
throw new RuntimeException("login failed");
}

public User getUser(long id) {
return users.stream().filter(user -> user.getId() == id).findFirst().orElseThrow();
}

public User register(String email, String pd, String name) {
users.forEach(user -> {
if (user.getEmail().equalsIgnoreCase(email)) {
throw new RuntimeException("user already exists!");
}
});
User user = new User(users.stream().mapToLong(User::getId).max().getAsLong() + 1, email, pd, name);
users.add(user);
mailService.sendRegistrationMail(user);
return user;
}
}

配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
@ComponentScan
@Configuration
//@EnableAspectJAutoProxy
public class AppConfig {

public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
User user = userService.register("libai@gmail.com", "libai", "liBai");
System.out.println(user);
userService.login(user.getEmail(), user.getPassword());
}
}

此时运行的结果是:

Hello liBai!
User{id=‘4’, email=‘libai@gmail.com’, password=‘libai’, name=‘liBai’}
Hi, liBai! You are login at 2020-12-22T16:52:05.487303

现在,我们准备给UserService的每个业务方法执行前添加日志,给MailService的每个业务方法执行前后添加日志:(需要给AppConfig加上@EnableAspectJAutoProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
@Aspect
public class LoginAspect {

@Before("execution(public * com.kanxz.spring5.service.UserService.*(..))")
public void doAccessCheck() {
System.err.println("[Before] do access check...");
}

@Around("execution(public * com.kanxz.spring5.service.UserService.*(..))")
public Object doLogging(ProceedingJoinPoint point) throws Throwable {
System.err.println("[Around] start; " + point.getSignature());
Object proceed = point.proceed();
System.err.println("[Around] end; " + point.getSignature());
return proceed;
}
}

此时运行结果:

[Around] start; User com.kanxz.spring5.service.UserService.register(String,String,String)
[Before] do access check…
Hello liBai!
[Around] end; User com.kanxz.spring5.service.UserService.register(String,String,String)
User{id=‘4’, email=‘libai@gmail.com’, password=‘libai’, name=‘liBai’}
[Around] start; User com.kanxz.spring5.service.UserService.login(String,String)
[Before] do access check…
Hi, liBai! You are login at 2020-12-22T17:01:03.7016413
[Around] end; User com.kanxz.spring5.service.UserService.login(String,String)

观察doAccessCheck()方法,我们定义了一个@Before注解,后面的字符串是告诉AspectJ应该在何处执行该方法,这里写的意思是:执行UserService的每个public方法前执行doAccessCheck()代码。

再观察doLogging()方法,我们定义了一个@Around注解,它和@Before不同,@Around可以决定是否执行目标方法,因此,我们在doLogging()内部先打印日志,再调用方法,最后打印日志后返回结果。

LoggingAspect类的声明处,除了用@Component表示它本身也是一个Bean外,我们再加上@Aspect注解,表示它的@Before标注的方法需要注入到UserService的每个public方法执行前,@Around标注的方法需要注入到MailService的每个public方法执行前后。

再来看看execution语法:

execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)

其中:返回类型模式、方法名模式、参数模式是必选项。

execution(public * com.kanxz.spring5.service.UserService.*(..))

  • public:修饰符模式
  • *:所有返回类型
  • com.kanxz.spring5.service.UserService.*:表示UserService类中的所有方法
  • (..)代表任意参数

使用注解装配AOP

我们以一个实际例子演示如何使用注解实现AOP装配。

目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|-- pom.xml
`-- src
|-- main
| |-- java
| | `-- com
| | `-- kanxz
| | `-- spring5
| | |-- AppConfig.java
| | |-- metrics
| | | |-- MetricAspect.java
| | | `-- MetricTime.java
| | `-- service
| | |-- MailService.java
| | |-- User.java
| | `-- UserService.java
| `-- resources
`-- test
`-- java

为了监控应用程序的性能,我们定义一个性能监控的注解:

1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MetricTime {
String value();
}

Retention注解

@Retention(保留)注解说明这种类型的注解会被保留到某个阶段,有三个值:

  • RetentionPolicy.SOURCE —— 这种类型的Annotations只在源代码级别保留,编译时就会被忽略

  • RetentionPolicy.CLASS —— 这种类型的Annotations编译时被保留,在class文件中存在,但JVM将会忽略

  • RetentionPolicy.RUNTIME —— 这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用

Target注解

@Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。

作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)

取值(ElementType)有:

  1. CONSTRUCTOR:用于描述构造器
  2. FIELD:用于描述域
  3. LOCAL_VARIABLE:用于描述局部变量
  4. METHOD:用于描述方法
  5. PACKAGE:用于描述包
  6. PARAMETER:用于描述参数
  7. TYPE:用于描述类、接口(包括注解类型) 或enum声明

在需要被监控的关键方法上标注该注解:

1
2
3
4
5
6
7
8
9
@Component
public class UserService {
// 监控register()方法性能:
@MetricTime("register")
public User register(String email, String password, String name) {
...
}
...
}

然后,我们定义MetricAspect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Aspect
@Component
public class MetricAspect {

@Around("@annotation(metricTime)")
public Object metric(ProceedingJoinPoint joinPoint, MetricTime metricTime) throws Throwable {
String name = metricTime.value();
long start = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long t = System.currentTimeMillis() - start;
// 写入日志或发送至JMX:
System.err.println("[Metrics] " + name + ": " + t + "ms");
}
}
}

注意metric()方法标注了@Around("@annotation(metricTime)"),它的意思是,符合条件的目标方法是带有@MetricTime注解的方法,因为metric()方法参数类型是MetricTime(注意参数名是metricTime不是MetricTime),我们通过它获取性能监控的名称。

有了@MetricTime注解,再配合MetricAspect,任何Bean,只要方法标注了@MetricTime注解,就可以自动实现性能监控。

AOP避坑指南


数据库

在前面学习JDBC编程时学到,Java程序使用JDBC接口访问关系数据库的时候,需要以下几步:

  • 创建全局DataSource实例,表示数据库连接池;
  • 在需要读写数据库的方法内部,按如下步骤访问数据库:
    • 从全局DataSource实例获取Connection实例;
    • 通过Connection实例创建PreparedStatement实例;
    • 执行SQL语句,如果是查询,则通过ResultSet读取结果集,如果是修改,则获得int结果。

在Spring使用JDBC,首先我们通过IoC容器创建并管理一个DataSource实例,然后,Spring提供了一个JdbcTemplate,可以方便地让我们操作JDBC,因此,通常情况下,我们会实例化一个JdbcTemplate

来看一个例子:

目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|-- pom.xml
`-- src
|-- main
| |-- java
| | `-- com
| | `-- kanxz
| | `-- spring5
| | `-- jdbc
| | |-- JDBCConfig.java
| | |-- JDBCService.java
| | `-- User.java
| `-- resources
| `-- jdbc.properties
`-- test
`-- java

首先添加maven依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.12.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.3</version>
</dependency>

在JDBCConfig中,我们需要创建以下几个必须的Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@ComponentScan
@Configuration
@PropertySource("/jdbc.properties")
public class JDBCConfig {
@Value("${jdbc.url}")
private String url;

@Value("${jdbc.username}")
private String username;

@Value("${jdbc.password}")
private String password;

@Value("${jdbc.driverClassName}")
private String driverClassName;

@Bean
DataSource createDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverClassName);
return dataSource;
}

@Bean
JdbcTemplate createJdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}

其中,配置文件为:

1
2
3
4
jdbc.url=jdbc:mysql:///user_db?serverTimezone=UTC
jdbc.username=root
jdbc.password=
jdbc.driverClassName=com.mysql.cj.jdbc.Driver

新建好数据库user_db,表users,含有id, email, password, name

建立对应的User类,设置相应的set, get方法

增删改

调用 JdbcTemplate对象里面 update方法实现增删改操作:

1
public int update(String sql, @Nullable Object... args)

有两个参数:sql语句,和可变参数(用于填充sql语句)

返回影响的行数

JDBCService类添加各种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Component
public class JDBCService {
JdbcTemplate jdbcTemplate;

@Autowired
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public void addUser(User user) {
String sql = "insert into users value (?, ?, ?, ?)";
int update = jdbcTemplate.update(sql, user.getId(), user.getEmail(), user.getPassword(), user.getName());
System.out.println("添加了" + update + "条数据");
}

public void updateUser(User user) {
String sql = "update users set email = ?, password = ?, name = ? where id = ?";
int update = jdbcTemplate.update(sql, user.getEmail(), user.getPassword(), user.getName(), user.getId());
System.out.println("修改了" + update + "条数据");
}

public void delUser(int id) {
String sql = "delete from users where id = ?";
int update = jdbcTemplate.update(sql, id);
System.out.println("删除了" + update + "条数据");
}
}

查询

查询返回值

1
public <T> T queryForObject(String sql, Class<T> requiredType)

有两个参数 :第一个参数:sql 语句, 第二个参数:返回类型 Class

1
2
3
4
5
public int selectCount() {
String sql = "select count(*) from users";
int count = jdbcTemplate.queryForObject(sql, Integer.class);
return count;
}

查询返回对象

1
public <T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args)

有三个参数 :第一个参数:sql 语句, 第二个参数:RowMapper 是接口,针对返回不同类型数据,使用这个接口里面实现类完成数据封装,第三个参数:sql 语句值

1
2
3
4
5
public User findUserById(int id) {
String sql = "select * from users where id = ?";
User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), id);
return user;
}

查询返回集合

1
public <T> List<T> query(String sql, RowMapper<T> rowMapper, @Nullable Object... args)

有三个参数 :第一个参数:sql 语句 , 第二个参数:RowMapper 是接口,针对返回不同类型数据,使用这个接口里面实现类完成数据封装 ,第三个参数:sql 语句值

1
2
3
4
5
public List<User> getAll() {
String sql = "select * from users";
List<User> lists = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
return lists;
}

事务


Spring5 整合 JUnit5

以上面数据库操作的测试为例:

原操作:

1
2
3
4
5
6
7
@Test
public void testAdd() {
ApplicationContext context = new AnnotationConfigApplicationContext(JDBCConfig.class);
JDBCService jdbcService = context.getBean(this.jdbcService.getClass());
User user = new User(3, "Mary@gmail.com", "pd", "Mary");
jdbcService.addUser(user);
}

现引入依赖:

1
2
3
4
5
6
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.12.RELEASE</version>
<scope>test</scope>
</dependency>

可以得到简化的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = JDBCConfig.class)
public class JDBCTest {

@Autowired
private JDBCService jdbcService;

@Test
public void testAdd() {
User user = new User(3, "Mary@gmail.com", "pd", "Mary");
jdbcService.addUser(user);
}

@Test
public void testUpdate() {
User user = new User(3, "Mary@gmail.com", "password", "Mary");
jdbcService.updateUser(user);
}

@Test
public void testDel() {
jdbcService.delUser(3);
}

@Test
public void testSelectCount() {
System.out.println(jdbcService.selectCount());
}

@Test
public void testFindUserById() {
User user = jdbcService.findUserById(2);
System.out.println(user);
}

@Test
public void testGetAll() {
List<User> users = jdbcService.getAll();
users.forEach(System.out::println);
}

}

学习自: