文中出现的所有代码,可在github下载。
一、 ioc
1.ioc思想
将对象的创建交由Spring框架进行管理,这种将传统对象的创建流程变为框架创建和管理的思想,叫做控制反转。
原本创建对象的方式,是通过new由程序控制的。
写一个userDao接口,用于去数据库查询用户信息
public interface UserDao {
void getUser();
}
由于项目数据库不一样,所以实现类有所区别,假设有SQL和MongoDB的实现(MongoDB略)。
public class UserDaoImplForSQL implements UserDao {
@Override
public void getUser() {
System.out.println("sql获取用户数据");
}
}
然后往上层走,就是服务层。
public interface UserService {
void getUserInfo();
}
服务层的实现
public class UserServiceImpl implements UserService {
// 这里调用dao层的具体实现存在变化
private UserDao userDao = new UserDaoImplForSQL();
@Override
public void getUserInfo() {
userDao.getUser();
}
}
然后是调用服务层,这里就是Test
public class Test {
public static void main(String[] args) {
UserService service = new UserServiceImpl();
service.getUserInfo();
}
}
这种方法,过于耦合,一旦实现类发生变化,就需要去更改程序。如果下次使用MongoDB了呢,那么UserServiceImpl里面的private UserDao userDao = new UserDaoImplForSQL()就需要手动去改为private UserDao userDao = new UserDaoImplForMongoDB()。
那么怎么把这种耦合给解开呢?利用学过多态的知识,留出一个接口,在service层里先不去实现,等到在用的时候,调用者去选择实现。
只需要加一个set方法,就能发生质的变化。
public class UserServiceImpl implements UserService {
// 调用dao层去获取用户信息,实现类获取会发生变化
//private UserDao userDao = new UserDaoImplForSQL();
private UserDao userDao;
// 把控制权交给调用方
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void getUserInfo() {
userDao.getUser();
}
}
这样,调用者就能自己去选择使用哪个实现类了,而不用程序员去修改程序,实现了解耦。
public class Test {
public static void main(String[] args) {
// UserService userService = new UserServiceImpl();
UserServiceImpl userService = new UserServiceImpl();
userService.setUserDao(new UserDaoImplForSQL());
userService.getUserInfo();
// 换一种实现方式
userService.setUserDao(new UserDaoImplForMongoDB());
userService.getUserInfo();
}
}
控制反转IoC(Inversion of Control),是一种设计思想,没有IoC的程序中 , 我们使用面向对象编程 , 对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方,个人认为所谓控制反转就是:获得依赖对象的方式反转了。
2.依赖注入(xml方法)实现ioc
Spring容器在初始化时先读取配置文件,根据配置文件或元数据创建与组织对象存入容器中,程序使用时再从Ioc容器中取出需要的对象。
什么是依赖注入(DI)
- 依赖:指Bean对象的创建依赖于容器,Bean对象的依赖资源
- 注入 : 指Bean对象所依赖的资源 , 由容器来设置和装配
假如有这样一个对象Hello,现在我们要把这个对象注入到容器当中,有两种注入方式。
public class Hello {
private String name;
public void show() {
System.out.println("Hello," + name);
}
}
2.1 构造器注入
注意这里的构造器注入指的是有参构造。
public class Hello {
private String name;
public Hello(String name) {
System.out.println("有参构造创建对象");
this.name = name;
}
public void show() {
System.out.println("Hello," + name);
}
}
然后在配置文件进行配置
<?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就是一个java对象,由spring创建和管理 -->
<bean id="hello" class="com.example.spring.Hello">
<constructor-arg name="name" value="spring"/>
</bean>
</beans>
这里的constructor-arg后面选用的是name,其实还能选用index(参数下标),type(参数类型),value表示具体的值,也能换成ref(引用)。
来看看怎么加载的配置文件吧,主要使用到了ClassPathXmlApplicationContext这个类。
public class MyTest {
public static void main(String[] args) {
//解析beans.xml文件 , 生成管理相应的Bean对象
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//getBean : 参数即为spring配置文件中bean的id
Hello hello = context.getBean("hello", Hello.class);
hello.show();
}
}
关于beans.xml配置文件
id是bean的唯一标识符(也可以不写),如果存在两个id一样的bean,无论别名是否相同,都会报错。
可以在id后面写name,当id无法匹配时,可以通过别名匹配,别名可以设置多个,用空格或者逗号隔开。
<bean id="hello" name="he llo" class="com.example.spring.Hello">
<constructor-arg name="name" value="spring"/>
</bean>
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Hello hello = context.getBean("llo", Hello.class);
hello.show();
id不写时,通过name匹配,id和name都不写时,通过全限定名匹配。
<bean class="com.example.spring.Hello">
<constructor-arg name="name" value="spring"/>
</bean>
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Hello hello = context.getBean(Hello.class);
hello.show();
也可以引入其他的beans.xml(团队合作)
<import resource="{path}/beans.xml"/>
2.2 set注入
使用set注入,可以注入很多属性(常量,引用,数组,List,Map等),构建一个类Student,看看这些是怎么注入的。
public class Student {
private String name;
private Address address;
private String[] books;
private List<String> hobbys;
private Map<String,String> card;
private Set<String> games;
private String wife;
//省略set方法
}
其中address引用为
public class Address {
private String address;
//省略set方法
}
看看beans文件的配置,非常简单
<!-- set注入 -->
<bean id="address" class="com.example.spring.Address">
<property name="address" value="sichuan"/>
</bean>
<bean id="student" class="com.example.spring.Student">
<property name="name" value="yang"/>
<property name="address" ref="address"/>
<property name="books">
<array>
<value>数据结构</value>
<value>操作系统</value>
<value>计算机网络</value>
</array>
</property>
<property name="hobbys">
<list>
<value>piano</value>
<value>running</value>
</list>
</property>
<property name="card">
<map>
<entry key="中国工商" value="156467865"/>
<entry key="中国银行" value="468765468"/>
</map>
</property>
<property name="games">
<set>
<value>overwatch</value>
<value>lol</value>
</set>
</property>
<property name="wife"><null/></property>
</bean>
再看看我们的输出吧
Student{name='yang', address=Address{address='sichuan'}, books=[数据结构, 操作系统, 计算机网络], hobbys=[piano, running], card={中国工商=156467865, 中国银行=468765468}, games=[overwatch, lol], wife='null'}
2.3 p命名和c命名注入
看起来像是第三种方式,其实分别还是和上述两种原理一样,c(构造: Constructor)命名注入使用有参构造,p(属性: properties)命名注入使用set注入。
分别需要在xml文件头部加上约束
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
这里就只放xml文件了,毕竟原理和上述两种几乎一样,变的只是约束规则。
<!-- p命名注入 -->
<bean name="user" class="com.example.spring.User" p:name="yang" p:age="20"/>
<!-- c命名注入 -->
<bean name="user" class="com.example.spring.User" c:name="huang" c:age="18"/>
3.Bean的作用域
spring的bean是通过工厂模式创建的,读取配置文件后,spring会创建配置文件里面的所有单例对象,getBean()操作就类似于工厂的create方法。
在Spring中,那些组成应用程序的主体及由Spring IoC容器所管理的对象,被称之为bean。简单地讲,bean就是由IoC容器初始化、装配及管理的对象。
配置方法,在bean标签里输入scope即可选择。
bean存在4种作用域
- singleton: 单例,在spring ioc容器仅存在一个bean实例,默认。
- prototype:多例,每次调用getBean()时,都会返回一个新的实例。
- request:每次http请求都会创建一个新的bean,当处理请求结束,request作用域的bean实例将被销毁。
- session:同一个http session共享一个bean,不同session使用不同bean。
注意:
- request、session作用域只能用在基于web的Spring ApplicationContext环境。
- singleton在创建起容器时就同时自动创建了一个bean的对象,不管你是否使用,他都存在了(饿汉式)。
- prototype在每次对该bean请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)时都会创建一个新的bean实例(懒汉式)。
那么问题来了,bean默认是单例的,那么如何保证并发安全?
某些情况下,单例是并发不安全的,如下所示,多个请求来临,进入的都是同一个单例Controller对象,并对成员变量的值进行修改,相互影响。
@Controller
public class HomeController {
private int i;
@GetMapping("testsingleton1")
@ResponseBody
public int test1() {
return ++i;
}
}
解决办法:
1.避免使用成员变量
2.单例变原型
对 web 项目,可以 Controller 类上加注解 @Scope(“prototype”) 或 @Scope(“request”),对非 web 项目,在 Component 类上添加注解 @Scope(“prototype”) 。
但这种方法会产生大量对象。
3.使用并发安全的类,像当前场景可以使用AtomicInteger。
4.有人说可以使用线程隔离类 ThreadLocal,这种方式可以达到线程隔离,但还是无法达到并发安全。一旦存在线程复用,那么两个线程还是会拿到同样的成员变量。
4.Bean的自动装配
有以下场景,两个类dog和cat,只有一个shout方法,然后master引用这两个类并调用shout方法
public class Cat {
public void shout() {
System.out.println("miao");
}
}
public class Dog {
public void shout() {
System.out.println("wang");
}
}
public class Master {
private Cat cat;
private Dog dog;
private String str;
//...
}
按照之前的逻辑,配置文件应该这样写
<bean id="dog" class="com.example.spring.ioc.Dog"/>
<bean id="cat" class="com.example.spring.ioc.Cat"/>
<bean id="master" class="com.example.spring.ioc.Master">
<property name="dog" ref="dog"/>
<property name="cat" ref="cat"/>
<property name="str" value="yang"/>
</bean>
主方法如下
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Master master = context.getBean("master", Master.class);
master.getCat().shout();
master.getDog().shout();
这种模式常常会出现拼写错误,采用自动装配将避免这些人为造成的错误,并使配置简单化。
自动装配分为两种,按名称(byName)与按类型(byType),具体二者的意思看下面实例。
4.1 xml自动装配
先来看看byName,我们修改配置文件如下(dog和cat都不用我们手动去指明引用)。
<bean id="master" class="com.example.spring.ioc.Master" autowire="byName">
<property name="str" value="yang"/>
</bean>
发现代码能正常跑起来,这是因为byName自动装配,spring会从上下文去找到与setName相同的name(bean的id或者name),举个栗子,Master具有setDog方法,那么在自动装配的时候,spring就在xml文件中找bean id为dog的bean,这也是byName名字的由来,如果名字不匹配,也就找不到bean了。
再来看看byTpye,顾名思义,也就是根据类型来找bean进行装配。但要注意,使用autowire byType首先需要保证:同一类型的对象,在spring容器中唯一。如果不唯一,会报不唯一的异常。
<bean id="master" class="com.example.spring.ioc.Master" autowire="byType">
<property name="str" value="yang"/>
</bean>
4.2 注解自动装配
jdk1.5开始支持注解,spring2.5开始全面支持注解。
首先要在xml文件头引入
xmlns:context="http://www.springframework.org/schema/context"
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
然后,开启注解支持
<context:annotation-config/>
4.2.1 @Autowire
@Autowire是根据类型(byType)自动装配,但又不依赖于set方法(应该是apo支持),我们这里去掉Master的set方法,只在变量上加入注解,并且xml配置如下。
<bean id="dog1" class="com.example.spring.ioc.Dog"/>
<bean id="cat2" class="com.example.spring.ioc.Cat"/>
<bean id="master" class="com.example.spring.ioc.Master"/>
public class Master {
@Autowired
private Cat cat;
@Autowired
private Dog dog;
private String str;
public Cat getCat() {
return cat;
}
public Dog getDog() {
return dog;
}
public String getStr() {
return str;
}
}
主方法能够正常运行。
@Autowired(required=false) 说明:false,对象可以为null;true,对象必须存对象,不能为null。
4.2.2 @Qualifier
@Qualifier作为与@Autowire搭配使用的注解,可以支持byName装配,以下配置,在只用@Autowire肯定会报错(存在两个一模一样的类型)
<bean id="dog1" class="com.example.spring.ioc.Dog"/>
<bean id="dog2" class="com.example.spring.ioc.Dog"/>
<bean id="cat2" class="com.example.spring.ioc.Cat"/>
而使用@Qualifier,则可以指定选用哪一个Bean
@Autowired(required = false)
private Cat cat;
@Autowired(required = false)
@Qualifier(value = "dog1")
private Dog dog;
4.2.3 @Resource
@Resource默认byName自动装配,可以默认name(就是属性名),也可以指定name
@Resource
private Cat cat;
@Resource(name = "dog1")
private Dog dog;
如果byName失败,则进行byType匹配,如果都没找到,抛出异常。
@Autowire与@Resource的异同
- @Autowired与@Resource都可以用来装配bean。都可以写在字段上,或写在setter方法上。
- @Autowired默认按类型装配(属于spring规范),默认情况下必须要求依赖对象必须存在,如果要允许null 值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用
- @Resource(属于J2EE复返),默认按照名称进行装配,名称可以通过name属性进行指定。如果没有指定name属性,当注解写在字段上时,默认取字段名进行按照名称查找,如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。
总的来说,@Autowired先byType,@Resource先byName。
5.使用注解开发
可以发现,上一章让配置文件的配置越来越少,只需要配置Bean就好了,连属性都能自动注入,那么能不能连Bean也不用我们配置了呢?这一章,我们彻底抛弃xml,使用注解开发!
5.1 Bean的实现
首先,要在配置文件里引入注解的约束-context,约束和4.2一样。
xmlns:context="http://www.springframework.org/schema/context"
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
同时要在配置文件加入注解扫描包,该包里注解注入的类会被加入ioc容器。
<!--指定注解扫描包-->
<context:component-scan base-package="com.example.spring"/>
然后,我们就做xml里面bean所作的事,在扫描包下创建一个类SpUser,并且加上注解@Component。
@Component("spUser")
// 相当于配置文件中 <bean id="spUser" class="当前注解的类"/>
public class SpUser {
public String name = "yang";
}
这样就相当于完成xml文件里<bean>标签的工作了。
5.2 属性注入
同样,属性也可以通过注解来设置值,相当于<property>标签。
@Component("spUser")
// 相当于配置文件中 <bean id="spUser" class="当前注解的类"/>
public class SpUser {
@Value("yang")
// 相当于配置文件中 <property name="name" value="yang"/>
public String name;
}
@Value标签也能加在set方法上,效果一样。
@Component("spUser")
// 相当于配置文件中 <bean id="spUser" class="当前注解的类"/>
public class SpUser {
public String name;
@Value("yang")
public void setName(String name) {
this.name = name;
}
}
5.3 衍生注解
开发中,经常还会看到其他三个注解
- @Controller,web层
- @Service,service层
- @Repository,dao层
它们的功能其实是一样的,都是@Componet的衍生,目的是为了让程序员区分该类属于哪一层,便于开发。
此外,既然不使用xml,其他功能也就有对应的注解对应,像是bean的属性scope,也能通过注解实现。
@Component("spUser")
// 相当于配置文件中 <bean id="spUser" class="当前注解的类"/>
@Scope("prototype")
public class SpUser {
public String name;
}
5.4 xml与注解开发比较
- xml结构清晰,维护方便
- 注解适合小型项目,快速开发
- 推荐二者结合使用:使用xml管理Bean,使用注解完成属性注入
5.5 基于java类进行配置
我们已经从xml里面丢掉了<property>,<bean>,这回,我们直接把xml全部丢掉,使用java配置类实现类似xml的功能。
先创建一个类
@Component
public class SpDog {
@Value("wang")
public String name;
}
然后配置我们的配置类
@Configuration //代表这是一个配置类
public class MyConfig {
@Bean //通过方法注册一个bean,这里的返回值就Bean的类型,方法名就是bean的id!
public SpDog spDog() {
return new SpDog();
}
}
测试,用到的是AnnotationConfigApplicationContext
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);
SpDog spDog = applicationContext.getBean("spDog",SpDog.class);
System.out.println(spDog.name);
特别的,使用@Import可导入合并其他配置类。
这种配置方法在springboot中非常常见。
二、aop
1.静态代理模式
1.1 代理模式思想
我们以租房这种场景为例,假设房东有套房要出租,但是他又觉得出租这件事太麻烦,又要去找租客又要收房租,所以找了中介,去代理房东处理租房事务(找租客,带租客看放,收租金等),接下来,小明找到了中介,并且成功租到了房子,这就是一个代理模式。
在程序中,代理模式可以表示为以下图的关系
这里存在四个角色
- 抽象角色 : 一般使用接口或者抽象类来实现
- 真实角色 : 被代理的角色
- 代理角色 : 代理真实角色 ; 代理真实角色后 , 一般会做一些附属的操作
- 客户 : 使用代理角色来进行一些操作
我们把租房的场景套入代理模式
先从抽象角色开始,创建一个接口Rent.class,代表租房这件事。
// 抽象角色 租房
public interface Rent {
void rent();
}
然后被代理的角色是房东,实现Rent接口,他要出租房子
public class Host implements Rent{
@Override
public void rent() {
System.out.println("房东出租房子");
}
}
代理角色是中介,实现Rent接口,他负责房子的真正的出租
public class Proxy implements Rent{
// 中介代理房东完成租房
private Host host;
public Proxy(Host host) {
this.host = host;
}
@Override
public void rent() {
seeHouse();
host.rent();
fare();
}
private void seeHouse() {
System.out.println("中介带人看房子");
}
private void fare() {
System.out.println("收取中介费");
}
}
最后是客户,客户找到中介租房子
public class Client {
public static void main(String[] args) {
Host host = new Host();
Proxy proxy = new Proxy(host);
proxy.rent();
}
}
执行结果如下
中介带人看房子
房东出租房子
收取中介费
在这个过程中,客户直接打交道的是中介,看不到房东,也没有办法直接与房东打交道,房东把事情全部交给了中介,中介代理一切租房事务,这就是代理模式。
1.2 静态代理的应用–日志
理解了代理模式的思想,我们来看一种常见的应用场景–加日志。
假如有如下代码,已经实现了增删查改的功能。
public interface UserService {
void add();
void delete();
void query();
void update();
}
public class UserServiceImpl implements UserService{
@Override
public void add() {
System.out.println("增加用户");
}
@Override
public void delete() {
System.out.println("删除用户");
}
@Override
public void query() {
System.out.println("查询用户");
}
@Override
public void update() {
System.out.println("更新用户");
}
}
现在的需求是,需要每次操作的时候,都要加上日志记录。
一般开发来说,都不建议修改原来的代码,这时候,就可以用到代理模式。把上述的UserService看作是抽象角色,UserServiceImpl看作是真实角色,那么,根据上一小节的介绍,我们只需要构造一个代理角色代理UserServiceImpl即可。
public class UserServiceProxy implements UserService{
UserServiceImpl userServiceImpl;
public UserServiceProxy(UserServiceImpl userServiceImpl) {
this.userServiceImpl = userServiceImpl;
}
@Override
public void add() {
System.out.println("log:add");
userServiceImpl.add();
}
@Override
public void delete() {
System.out.println("log:delete");
userServiceImpl.delete();
}
@Override
public void query() {
System.out.println("log:query");
userServiceImpl.query();
}
@Override
public void update() {
System.out.println("log:update");
userServiceImpl.update();
}
}
这样,就可以在不改动原来代码的情况下,加上了日志。
public class UserClient {
public static void main(String[] args) {
// 被代理类
UserServiceImpl userServiceImpl = new UserServiceImpl();
// 交给代理去做
UserService proxy = new UserServiceProxy(userServiceImpl);
proxy.add();
proxy.query();
}
}
结果如下所示
log:add
增加用户
log:query
查询用户
1.3 aop思想
我们在不改动原代码的条件下,实现了对原有功能的增强,这就是aop的核心思想。
平时我们的业务都是纵向开发,而aop则是应用代理模式,实现横向的功能增强,如下图所示
2.动态代理模式
2.1 改进日志增强
静态代理虽然能实现代理的思想,但是有一个致命缺点,每次代理就会产生一个代理类,随着开发的深入,代理类会越来越多。能不能一劳永逸,灵活的产生代理类呢?这就引出了动态代理。
动态代理相较于静态代理,仅仅只改动了一个地方,那就是动态代理的代理角色是动态生成的,而不是像静态代理那样由程序员写好的,这就可以代理有相同功能增强需求的一类对象。
还是来看加日志这种情景,假如我们要给所有的操作都加上日志,静态代理明显不够用,这时可以用以下方法改进代理类。
public class ProxyInvocationHandler implements InvocationHandler {
private Object target;
public void setTarget(Object rent) {
this.target = rent;
}
// 生成代理类
public Object getProxy() {
return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log(method.getName());
// 核心:利用反射实现
Object result = method.invoke(target, args);
return result;
}
//记录日志
public void log(String methodName) {
System.out.println("执行了" + methodName + "方法");
}
}
没看懂,没有关系,我们保留UserService和UserServiceImpl这两个类,将原本的代理类UserServiceProxy改成ProxyInvocationHandler,看看主程序怎么调用
public class UserClient {
public static void main(String[] args) {
// 真实对象
UserServiceImpl userServiceImpl = new UserServiceImpl();
// 代理对象调用处理程序
ProxyInvocationHandler pih = new ProxyInvocationHandler();
// 设置要代理的对象
pih.setTarget(userServiceImpl);
// 生成动态代理类
UserService proxy = (UserService) pih.getProxy();
proxy.add();
proxy.delete();
}
}
实现效果
执行了add方法
增加用户
执行了delete方法
删除用户
从pih.setTarget(userServiceImpl)
我们可以看到,直到使用的时候,我们才知道要代理的对象,而target
在ProxyInvocationHandler
里面是Object类型,在pih.getProxy()
的时候动态的产生一个代理类,而这个代理类是使用反射产生的(target.getClass().getInterfaces()
),这就不难理解为什么能够动态生成代理类,并且和代理对象同属一个抽象接口。
当在调用代理类的方法时(比如这里add和delete),会调用处理程序的invoke方法,传入代理对象、方法和参数,在这里实现功能增强,决定何时调用真实对象的方法以及实现aop的前后环绕。
这样,任何类通过通过这种方法,都能加上日志功能。以上我们用的是JDK动态代理技术(基于接口),还有其他的技术像是cglib(基于类),javasis等,但是其思想都是一样的。
2.2 动态代理和静态代理的好处
以下加粗的代表动态代理的好处:
- 可以使得我们的真实角色更加纯粹 . 不再去关注一些公共的事情
- 公共的业务由代理来完成 . 实现了业务的分工
- 公共业务发生扩展时变得更加集中和方便
- 一个动态代理 , 一般代理某一类业务
- 一个动态代理可以代理多个类,代理的是接口!
3.aop
3.1 什么是aop
AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
在学习AOP之前,我们需要了解以下名词定义
- 横切关注点:跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 , 安全 , 缓存 , 事务等等
- 切面(ASPECT):横切关注点 被模块化 的特殊对象。即,它是一个类。
- 通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。
- 目标(Target):被通知对象。
- 代理(Proxy):向目标对象应用通知之后创建的对象。
- 切入点(PointCut):切面通知 执行的 “地点”的定义。
- 连接点(JointPoint):与切入点匹配的执行点。
如图所示,代表一个目标对象被增强的过程,这里要理解切点与切面的关系(后续实战会细讲)。
特别的,我们的通知(Advice)具有以下几种:
- 前置通知,实现接口:MethodBeforeAdvice
- 后置通知,实现接口:AfterReturingAdvice
- 环绕通知,实现接口:MethodInterceptor
- 异常抛出通知,实现接口:ThrowsAdvice
- 引介通知,实现接口:IntroductionInterceptor
下面我们来将一下几种实现aop的方法,同样以上面的日志场景为例,UserService和UserServiceImpl不做任何改变,不管是哪一种方法,都需要导入aop的依赖包
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
3.2 Spring API 实现实现aop
利用aop包下写好的模板接口,实现aop。
首先需要写增强类,我们写一个前置增强,一个后置增强
// 前置增强类
public class Log implements MethodBeforeAdvice {
//method : 要执行的目标对象的方法
//args : 被调用的方法的参数
//target : 目标对象
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println(target.getClass().getName() + "的" + method.getName() + "方法被执行了");
}
}
//后置增强类
public class AfterLog implements AfterReturningAdvice {
//returnValue 返回值
//method被调用的方法
//args 被调用的方法的对象的参数
//target 被调用的目标对象
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("执行了" + target.getClass().getName() + "的" + method.getName() + "方法,返回值:" + returnValue);
}
}
可以看到,增强类与实际目标类完全解耦,根本不用关心目标类在做什么。
现在,去xml配置,首先要导入aop约束
xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
然后把三个类注入bean
<bean id="userService" class="com.example.spring.aop.UserServiceImpl"/>
<bean id="log" class="com.example.spring.aop.Log"/>
<bean id="afterLog" class="com.example.spring.aop.AfterLog"/>
接下来,就是配置aop增强了,首先我们要明确切面与切点的概念,切点是需要切入的位置,就是这里的add,delete,update,query方法,而切面是增强的方法,也就是Log与AfterLog,我们要做的,就是把切面切到切点中去。
<aop:config>
<!-- 切入点 expression:表达式匹配要执行的方法 execution要执行的位置(修饰词 返回值 类名 方法名 参数)-->
<aop:pointcut id="pointcut" expression="execution(* com.example.spring.aop.UserServiceImpl.*(..))"/>
<!-- 执行通知 advice-ref代表要执行的通知 pointcut代表切入点 -->
<aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
</aop:config>
关于execution,这里的第一个*代表所有修饰词(public等,然后是类,第二个*表示所有方法,最后是方法参数。
测试一下效果
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
com.example.spring.aop.UserServiceImpl的add方法被执行了
增加用户
执行了com.example.spring.aop.UserServiceImpl的add方法,返回值:null
3.3 自定义类来实现aop
首先自己写一个切面
public class DiyAspect {
public void before() {
System.out.println("---方法执行前---");
}
public void after() {
System.out.println("---方法执行后---");
}
}
然后在配置文件定义切面和切点,并将切面切入切点。
<!-- 切面 -->
<bean id="diy" class="com.example.spring.aop.DiyAspect"/>
<aop:config>
<aop:aspect ref="diy">
<!-- 切点 -->
<aop:pointcut id="diyPointCut" expression="execution(* com.example.spring.aop.UserServiceImpl.*(..))"/>
<!-- 切面切入切点 -->
<aop:before method="before" pointcut-ref="diyPointCut"/>
<aop:after method="after" pointcut-ref="diyPointCut"/>
</aop:aspect>
</aop:config>
3.4 注解实现aop
直接使用一个类实现xml的所有工作。
@Component
@Aspect
public class AnnotationAspect {
@Before("execution(* com.example.spring.aop.UserServiceImpl.*(..))")
public void before() {
System.out.println("---方法执行前---");
}
@After("execution(* com.example.spring.aop.UserServiceImpl.*(..))")
public void after() {
System.out.println("---方法执行后---");
}
@Around("execution(* com.example.spring.aop.UserServiceImpl.*(..))")
public void around(ProceedingJoinPoint pj) throws Throwable {
System.out.println("环绕前");
// 执行目标方法proceed
Object o = pj.proceed();
System.out.println("环绕后");
System.out.println(o);
}
}
环绕前
---方法执行前---
增加用户
---方法执行后---
环绕后
null
3.5 aop总结
aop就是将公共的业务 (日志 , 安全等) 和领域业务结合起来 , 当执行领域业务时 , 将会把公共业务加进来 . 实现公共业务的重复利用 . 领域业务更纯粹 , 程序猿专注领域业务 , 其本质还是动态代理。