Spring框架是Java应用最广的框架,它的成功来源于理念,而不是技术本身,它的理念包括IoC(Inversion of Control,控制反转)和AOP(Aspect Oriented Programming,面向切面编程)。
一、Spring
1-1、简介
- 2002年,首次推出Spring框架的雏形:interface21框架。
- Spring框架即以interface21框架为基础经过重新设计,并不断丰富其内涵,于2004年3月24日发布了1.0正式版。
- Rod Johnson,Spring Framework创始人。
- Spring理念:使现有的技术实现更加容易,本身是一个大杂烩,整合了现有的技术框架。
- SSH:Struts2+Spring+Hibernate
- SSM:Spring MVC+Spring+Mybatis
官网:https://spring.io/projects/spring-framework
官方下载地址:https://repo.spring.io/release/org/springframework/spring/
GitHub:https://github.com/spring-projects/spring-framework
1-2、优点
Spring是一个轻量级的控制反转(IOC)和面向切面编程(AOP)的框架。
- Spring是一个开源免费的框架(容器)。
- Spring是一个轻量级的、非入侵式的框架。
- 控制反转(IOC),面向切面编程(AOP)。
- 支持事务的处理,对框架整合的支持。
轻量
Spring框架使用的jar包都比较小,一般在1M以下或者几百K。Spring核心功能所需的jar包总共在3M左右。
Spring框架运行占用的资源少,运行效率高。不依赖其它jar包。
1-3、组成
Spring七大模块
1-4、拓展
Spring Boot:构建一切
Spring Cloud:协调一切
Spring Cloud Data Flow:连接一切
- Spring Boot
- 一个快速开发的脚手架
- 基于Spring Boot可以快速开发单个微服务
- 约定大于配置
- Spring Cloud
- SpringCloud是基于Spring Boot实现的
二、IOC理论推导
- UserDao接口
- UserDaoImpl实现类
- UserService业务接口
- UserServiceImpl业务实现类
使用set接口实现:
private UserDao userDao;// = new UserDaoOracleImpl();
//利用set进行多态实现值的注入
public void setUserDao(UserDao userDao){
this.userDao = userDao;
}
UserDao userDao = new UserDaoOracleImpl();
程序是主动创建对象。- 使用
set注入
后,程序不再具有主动性,而是变成了被动接受对象。
这种思想,从本质上解决了问题,我们不用再去管理对象的创建了。系统的耦合性大大降低,可以更加专注在业务的实现上,这是IOC的原型。
2-1、IOC本质
控制反转IOC(Inversion of Control),是一种设计思想,DI(依赖注入Dependency Injection)是实现IOC的一种方法,也有人认为DI只是IOC的另一种说法。没有IOC的程序中,我们使用面向对象编程,对象的创建与对象的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方(所谓控制反转就是获得依赖的对象的方式反转了)。
● 依赖查找:DL ( Dependency Lookup ), 容器提供回调接口和上下文环境给组件。
● 依赖注入:DI (Dependency Injection),程序代码不做定位查询,这些工作由容器自行完成。
依赖注入:只需要在程序中提供要使用的对象名称就可以,至于对象如何在容器中创建、赋值、查找都由容器内部实现。
采用XML方式配置Bean的时候,Bean的定义信息是和实现分离的,而采用注解的方式可以把两者合为一体,Bean的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的。
控制反转是一种通过描述(XML或注解)并通过第三方去生产或获取特定对象的方式。在Spring中实现控制反转的是IOC容器,其实现方法式依赖注入(Dependency Injection,DI)。
Spring底层创建对象,使用的是反射机制。
2-2、IOC的体现
Servlet
创建类继承HTTPServlet
在web.xml中注册Servlet,使用
<servlet-name>myservlet</servlet-name> <servlet-class>com.lskj.controller.MyServlet</servlet-class>
没有创建Servlet对象。没有MyServlet myservlet = new MyServlet();
Servlet是Tomact服务器创建的,Tomact也称为容器。
Tomact作为容器,里面存放的有Servlet对象,Listener,Filter对象
三、HelloSpring
实现步骤:
1、导入spring依赖
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.0.RELEASE</version> </dependency>
(导入junit依赖,方便测试)
3、创建类:接口,实现类,没有接口的类
4、创建spring需要使用的配置文件,使用
<bean>
声明对象5、使用容器中的对象,通过ApplicationContext接口和它的实现类ClassPathXmlApplicationContext的方法getBean()
编写实体类
public class Hello { private String str; public String getStr() { return str; } public void setStr(String str) { this.str = str; } @Override public String toString() { return "Hello{" + "str='" + str + '\'' + '}'; } }
编写Spring文件beans.xml
<?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-3.0.xsd"> <!--使用Spring来创建对象,在Spring这些都称为Bean 类型 变量名 = new 类型(); Hello hello = new Hello(); bean = 对象 new Hello(); id = 变量名 class = new 的对象(类的全限定名称,不能时接口,因为spring时反射机制创建对象,必须使用类); property 相当于给对象中的属性设置一个值! Spring是把创建好的对象放入到map中,Spring框架有一个map存放对象。 springMap.put(id的值,对象); 例如:springMap.put("hello",new Hello()); 一个bean标签声明一个对象。 --> <bean id="hello" class="com.lskj.pojo.Hello"> <property name="str" value="Spring"></property> </bean> </beans>
测试
public class MyTest { @Test public void test01 { //获取Spring的上下文对象(spring默认的创建对象的时间:在创建spring的容器时,会创建配置文件中的所有的对象) ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); //对象现在都在Spring中管理,需要使用,直接去里面取出来就可以了 //getBean:参数即为Spring配置文件中bean的id Hello hello = (Hello) context.getBean("hello"); System.out.println(hello.toString()); } /** * 获取spring容器中Java对象的信息 */ @Test public void test02{ String config = "beans.xml"; ApplicationContext context = new ClassPathXmlApplicationContext(config); //使用spring提供的方法,获取容器中定义的对象的数量 System.out.println(context.getBeanDefinitionCount()); //容器中每个定义的对象的名称 String names[] = context.getBeanDefinitionNames(); for(String name:names){ System.out.println(name); } } }
Hello对象是谁创建的?
hello对象由Spring创建
Hello对象的属性是怎样设置的?
hello对象的属性是由Spring容器设置的
这个过程叫做控制反转:
控制:谁来控制对象的创建,传统应用程序的对象是由程序本身控制创建的,使用Spring后,对象是由Spring来创建的。【创建对象,对象的赋值,对象之间的关系管理】
反转:程序本身不创建对象,而是变成被动的接收对象。【把原来的开发人员管理,创建对象的权限转移给代码之外的容器实现。由容器代替开发人员管理对象。创建对象,给属性赋值。】
依赖注入:就是利用set方法来进行注入的。
正转:由开发人员在代码中,使用new构造方法创建对象,开发人员主动管理对象。
public static void main(String[] args){
Student student = new Student; //在代码中,创建对象。-->正转
}
Java中创建对象有哪些方式?
1.构造方法
2.反射
3.序列化
4.克隆
5.IOC:容器创建对象
6.动态代理
IOC是一种编程思想,由主动的编程变成被动的接收。
现在,彻底不需要改动程序,要实现不同的操作,只需要在XML配置文件中进行修改,所谓的IOC,就是对象由Spring来创建,管理,装配。
四、IOC创建对象的方式
使用无参构造创建对象,默认。
使用有参构造创建对象。
下标赋值
<!--第一种,下标赋值--> <bean id="user" class="com.lskj.pojo.User"> <constructor-arg index="0" value="浅笑安然"/> </bean>
类型
<!--第二种:通过类型创建,不建议使用--> <bean id="user" class="com.lskj.pojo.User"> <constructor-arg type="java.lang.String" value="lskj"/> </bean>
参数名
<!--第三种:直接通过参数名来设置--> <bean id="user" class="com.lskj.pojo.User"> <constructor-arg name="name" value="lskj"/> </bean>
在配置文件加载的时候,容器中管理的对象就已经初始化了。
五、Spring配置
5-1、别名
<!--别名,如果添加了别名,也可以使用别名获取到这个对象-->
<alias name="user" alias="user2"/>
5-2、Bean的配置
<!--
id:bean的唯一标识,相当于对象名
class:bean对象所对应的全限定名:包名+类型
name:也是别名,但name高级些,可以同时取多个别名(逗号,空格,分号分割,常用逗号)
-->
<bean id="user02" class="com.lskj.pojo.User02" name="userNew">
<property name="name" value="心若浮沉"/>
</bean>
5-3、import
import一般用于团队开发,它可以将多个配置文件,导入合并为一个。
假设,现项目中有多个人开发,每个人负责不同的类开发,不同的类需要注册到不同的bean中,可以利用import将所有人的beans.xml合并为一个。
beans.xml
beans2.xml
beans3.xml
applicationContext.xml
<!--语法--> <import resource="其它配置文件路径"/> <import resource="beans.xml"/> <import resource="beans2.xml"/> <import resource="beans3.xml"/> <!-- classpath: 表示类路径(class文件所在的目录) *:通配符,表示任意字符,主的配置文件名称不能包含在通配符的范围内。 --> <import resource="classpath:xx/*.xml"/>
使用时,直接使用总的配置即可。
六、依赖注入
6-1、构造器注入
IOC创建对象的方式提及。
1、<constructor-arg>
的name属性,name表示构造方法的形参名。
2、<constructor-arg>
的index属性,表示构造方法形参的位置。
6-2、set方式注入*
- 依赖注入:set注入。spring调用类的set方法实现属性赋值,也称设值注入。
- 依赖:bean对象的创建依赖于容器。
- 注入:bean对象中的所有属性,由容器来注入。
1、简单类型的set注入。
<property name="属性名" value="属性的值"/>
2、引用类型的set注入。
<property name="属性名" ref="bean的id"/>
【环境搭建】
复杂类型
public class Address { private int id; private String address; public int getId(){ return id; } public void setId(int id){ this.id = id; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } }
真实测试对象
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; private Properties info; //get //set //toString }
beans.xml
<?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-3.0.xsd"> <bean id="student" class="com.lskj.pojo.Student"> <!--第一种:普通值注入 value--> <property name="name" value="张三"></property> </bean> </beans>
测试类
public class MyTest { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); Student student = (Student) context.getBean("student"); System.out.println(student.getAddress()); } }
若实体类没有某个属性,但是存在对应的一个set方法,如下
public class User{ private int id; private String name; public User(){ System.out.println("Spring会调用类的无参数构造方法创建对象"); } public void setEmail(String email){ System.out.println("email = "+ email); } //get、set和toString方法 }
配置文件bean.xml放在resources/config目录下
<bean id="user" class="com.lskj.pojo.User"> <property name="id" value="1"/> <property name="name" value="张三"/> <!--根据命名规范,执行setEmail(String email)方法--> <property name="email" value="zhangsan@qq.com"/> </bean>
测试
@Test public void test(){ String config = "config/bean.xml"; ApplicationContext context = new ClassPathXmlApplicationContext(config); User user = (User)context.getBean("user"); System.out.println("user对象 = " + user); }
发现可正常运行。运行结果如下:
Spring会调用类的无参数构造方法创建对象 eamil = zhangsan@qq.com user对象 = User{id='1',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-3.0.xsd">
<bean id="student" class="com.lskj.pojo.Student">
<!--普通值注入 value-->
<property name="name" value="张三"></property>
<!--bean注入 ref 引用类型的set注入-->
<property name="address" ref="adderss"/>
<!--数组注入 ref-->
<property name="books">
<array>
<value>斗罗大陆</value>
<value>斗破苍穹</value>
<value>王者归来</value>
</array>
</property>
<!--List-->
<property name="hobbys">
<list>
<value>跑步</value>
<value>打球</value>
<value>K歌</value>
</list>
</property>
<!--Map-->
<property name="card">
<map>
<entry key="身份证" value="01234567899876543210"/>
<entry key="银行卡" value="12345678900987654321"/>
</map>
</property>
<!--Set-->
<property name="games">
<set>
<value>英雄联盟</value>
<value>王者荣耀</value>
<value>QQ飞车</value>
</set>
</property>
<!--null-->
<property name="wife">
<null/>
</property>
<!--Properties-->
<property name="info">
<props>
<prop key="学号">20200709</prop>
<prop key="姓名">李华</prop>
<prop key="性别">男</prop>
</props>
</property>
</bean>
<bean id="adderss" class="com.lskj.pojo.Address">
<property name="id" value="1"/>
<property name="address" value="xxx省xxx市xxx区xxx街道"/>
</bean>
</beans>
6-3、拓展方式注入
可以使用p命令空间和c命令空间进行注入。
使用:
<?xml version = "1.0" encoding = "UTF-8"?>
<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"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation = "http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<!--p命名空间注入,可以直接注入属性的值:property-->
<bean id="user" class="com.lskj.pojo.User" p:name="张三" p:age="20"/>
<!--c命名空间注入,通过构造器注入:construct-args-->
<bean id="user2" class="com.lskj.pojo.User" c:age="21" c:name="李四"/>
</beans>
测试:
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("userbeans.xml");
User user = context.getBean("user2", User.class);
System.out.println(user);
}
p命名和c命名空间不能直接使用,需要导入xml约束。
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
6-4、bean的作用域
单例模式 singleton(Spring默认机制)
<bean id="user2" class="com.lskj.pojo.User" c:age="21" c:name="李四" scope="session"/>
原型模式 prototype :每次从容器中get的时候,都会产生一个新对象
<bean id="user2" class="com.lskj.pojo.User" c:age="21" c:name="李四" scope="prototype"/>
request、session、application这些只能在web开发中使用到。
七、Bean的自动装配
自动装配是Spring满足bean依赖的一种方式。
Spring会在上下文中自动寻找,并自动给bean装配属性
在spring中有三种装配的方式
- 在xml中显示配置
- 在Java中显示配置
- 隐式的自动装配bean(引用类型的自动注入:spring框架根据某些规则可以给引用类型赋值)
7-1、测试
环境搭建:一个人拥有两个宠物。
7-2、ByName自动装配
byName(按名称注入):Java类中引用类型的属性名和spring容器中(配置文件)<bean>
的id名称一样,且数据类型是一致的,这样的容器中的bean,spring能够赋值给引用类型。
语法:
<bean id="xxx" class="xxx" autowire="byName">
<!--简单类型属性赋值-->
</bean>
<!--
byName;会自动在容器上下文中查找,和自己对象set方法后面的值对应的bean id
-->
<bean id="people" class="com.lskj.pojo.People" autowire="byName">
<property name="name" value="张三"/>
</bean>
<!------------------------------------------------------------------>
<bean id="student" class="com.lskj.pojo.Student" autowire="byName">
<!--普通值注入 value-->
<property name="name" value="张三"></property>
<!--bean注入 ref 引用类型的set注入-->
<!--<property name="address" ref="adderss"/>-->
</bean>
<bean id="adderss" class="com.lskj.pojo.Address">
<property name="id" value="1"/>
<property name="address" value="xxx省xxx市xxx区xxx街道"/>
</bean>
7-3、ByType自动装配
byType(按类型注入):Java类中引用类型的数据类型和spring容器(配置文件)<bean>
和class属性是同源关系的,这样的bean能够赋值给应用类型。
同源:
- Java类中引用类型的数据类型和bean的class的值是一样的。
- Java类中引用类型的数据类型和bean的class的值是父子类关系的。
- java类中引用类型的数据类型和bean的class的值是接口和实现类的关系的。
语法:
<bean id="xxx" class="xxx" autuwire="byType">
<!--简单类型属性赋值-->
</bean>
<bean class="com.lskj.pojo.Cat"/>
<bean class="com.lskj.pojo.Dog"/>
<!--
byName;会自动在容器上下文中查找,和自己对象set方法后面的值对应的bean id
byType:会自动在容器上下文中查找,和自己对象属性类型相同的bean
-->
<bean id="people" class="com.lskj.pojo.People" autowire="byType">
<property name="name" value="张三"/>
</bean>
- byName的时候,需要保证所有的bean唯一,并且这个bean需要和自动注入的属性的set方法的值一致。
- byType的时候,需要保证所有bean的class唯一,并且这个bean需要和自动注入的属性的类型一致。
7-4、使用注解实现自动装配
jdk1.5支持注解,spring2.5就支持注解了。
使用注解:
导入约束。(context约束)
配置注解的支持:
<context:annotation-config/>
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:annotation-config/> </beans>
@Autowired
直接在属性上使用,也可以在set方式上使用。
使用Autowired可以不用编写set方法,前提是这个自动装配的属性在IOC(Spring)容器中存在,且符合名字byname。
@Nullable 字段标记了这个注解,说明这个字段可以为null
public @interface Autowired {
boolean required() default true;
}
public class People {
//如果定义了Autowired的required属性为false,说明这个对象可以为null,否则不允许为空
@Autowired(required = false)
private Cat cat;
@Autowired
private Dog dog;
private String name;
}
如果@Autowired自动装配的环境比较复杂,自动装配无法通过一个注解【@Autowired】完成的时候,可以使用@Qualifier(value = “xxx”)去配置@Autowired的使用,指定一个唯一的bean对象注入。
public class People {
@Autowired
@Qualifier(value = "cat")
private Cat cat;
@Autowired
@Qualifier(value="dog")
private Dog dog;
private String name;
}
@Resource注解
public class People {
@Resource(name = "cat")
private Cat cat;
@Resource(name = "dog")
private Dog dog;
}
@Resource和@Autowired的区别;
都是用来自动装配的,都可以放在属性字段上
@Autowired通过byType的方式实现,而且必须要求这个对象存在。【常用】
@Resource默认通过byName的方式实现,如果找不到名字,则通过byType实现。如果两个都找不到的情况下,就报错。
执行顺序不同:@Autowired通过byType的方式实现。@Resource默认通过byName的方式实现。
八、使用注解开发
在Spring4之后,需要使用注解开发,必须保证aop的包导入了。
步骤:
1、导入 AOP 的 Jar 包。因为注解的后台实现用到了 AOP 编程。(导入依赖spring-context,包含aop的jar包)
2、需要更换配置文件头,即添加相应的约束。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> </beans>
3、需要在 Spring 配置文件中配置组件扫描器,用于在指定的基本包中扫描注解。
<!-- 声明组件扫描器(component-scan),组件就是Java对象 base-package:指定注解在项目中的包名 component-scan工作方式:spring会扫描遍历base-package指定的包,把包中和子包中的所有类,找到类中的注解,按照注解的功能创建对象,或给属性赋值 --> <context:component-scan base-package=""/> <!--指定多个包的三种方式--> <!--第一种:使用多次组件扫描器,指定不同包--> <context:component-scan base-package="com.lskj.xxx1"/> <context:component-scan base-package="com.lskj.xxx2"/> <!--第二种:使用分隔符(;或,)分隔多个包名--> <context:component-scan base-package="com.lskj.xxx1;com.lskj.xx2"/> <!--第三种:指定父包--> <context:component-scan base-package="com.lskj"/>
Spring给我们提供了
context:annotation-config
的简化的配置方式,自动帮助你完成声明,并且还自动搜索@Component , @Controller , @Service , @Repository等标注的类。
context:component-scan
除了具有context:annotation-config
的功能之外,context:component-scan
还可以在指定的package下扫描以及注册javabean 。还具有自动将带有@component,@service,@Repository等注解的对象注册到spring容器中的功能。因此当使用 context:component-scan 后,就可以将 context:annotation-config移除。
下面使用<context:annotation-config/>
进行演示
使用注解需要导入context约束,增加注解的支持。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
</beans>
- bean
属性如何注入
//等价于<bean id="user" class="com.lskj.pojo.User"/> //@Component 组件,若不指定对象名称,由spring提供默认名称:类名的首字母小写 @Component public class User { /*//相当于 <property name="name" value="lskj" @Value("lskj")*/ public String name; //相当于 <property name="name" value="lskj" @Value("lskj") public void setName(String name) { this.name = name; } }
衍生的注解
@Component衍生注解,在web开发中,会按照MVC三层框架分层
- dao:@Repository,创建dao对象,dao对象是能访问数据库的
- service:@Service,创建service对象,service对象是做业务处理的,可以有事务等功能
- controller:@Controller,创建控制器对象,能够接收用户提交的参数,显示请求的处理结果
这四个注解功能是一样的,都是代表将某个类注册到Spring容器中,装配Bean。
自动装配
简单类型的属性赋值
`@value` 属性:value是String类型的,表示见到那类型的属性值 位置:1)在属性定义的上面,无需set方法,推荐使用 2)在set方法的上面
引用类型赋值
`@Autowired`:自动装配通过类型,名字,默认使用的是byType自动注入。 如果Autowired不能唯一自动装配上属性,则需要通过@Qualifier(value="bean的id"):表示使用指定的名称的bean来完成赋值 @Autowired 还有一个属性 required,默认值为true,表示当匹配失败后,会终止程序运行。若将其值设置为false,@Autowired(required=false),则匹配失败,将被忽略,未匹配的属性值为 null 位置:1)在属性定义的上面,无需set方法,推荐使用 2)在set方法的上面 `@Resource`:JDK中的注解,spring框架提供了对这个注解的功能支持,可以使用这个注解给引用类型赋值 自动装配通过名字,类型,默认使用byName自动注入 位置:1)在属性定义的上面,无需set方法,推荐使用 2)在set方法的上面
@Resource 注解若不带任何参数,采用默认按名称的方式注入,按名称不能注入 bean,则会按照类型进行 Bean 的匹配注入
@Resource只使用byName方式,需要增加一个属性name
@Nullable
:字段标记了这个注解,说明这个字段可以为null作用域
@Component @Scope("singleton") public class User { /*//相当于 <property name="name" value="lskj" @Value("lskj")*/ public String name; //相当于 <property name="name" value="lskj" @Value("lskj") public void setName(String name) { this.name = name; } }
xml与注解:
- xml更加万能,适用于任何场合,维护简单方便
- 注解不是自己的类不能使用,维护相对复杂
xml与注解最佳实践:
xml用来管理bean
注解只负责完成属性的注入
使用过程中,只需要注意:必须让注解生效,需要开启注解的支持
<!--指定要扫描的包,这个包下的注解就会生效--> <context:component-scan base-package="com.lskj"/>
注解说明:
`@Autowired`:自动装配通过类型,名字
如果Autowired不能唯一自动装配上属性,则需要通过@Qualifier(value="xxx")
`@ Nullable`:字段标记了这个注解,说明这个字段可以为null
`@Resource`:自动装配通过名字,类型
`@Component`:组件,放在类上,说明这个类被Spring管理了,就是bean。
# 九、使用Java的方式配置Spring
完全不使用Spring的xml配置,全权交给Java来做。
JavaConfig是Spring的一个子项目,在Spring4之后,成为了一个核心功能。
实体类:
```java
//这个类被Spring接管了,注册到了容器中
@Component
public class User {
private String name;
public String getName() {
return name;
}
@Value("lskj") //属性注入值
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
配置文件:
@Configuration //Spring容器托管,注册到容器中,因为它是一个@Component,@Configuration 代表这个是一个配置类,相当于beans.xml
@ComponentScan("com.lskj.pojo")
@Import(Config02.class)
public class Config {
//注册一个bean,相当于bean标签
//方法的名字,相当于bean标签中的id属性
//方法的返回值,相当于bean标签中的class属性
@Bean
public User getUser(){
return new User(); //返回要注入到bean的对象
}
}
测试类:
public class MyTest {
public static void main(String[] args) {
//如果完全使用了配置类方式,就只能通过AnnotationConfig上下文来获取容器,通过配置类的class对象加载
ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
User getUser = context.getBean("getUser", User.class);
System.out.println(getUser.getName());
}
}
十、代理模式
是SpringAOP的底层。
代理模式的分类:
- 静态代理
- 动态代理
10-1、静态代理
角色分析:
- 抽象角色:一般会使用接口或者抽象类来解决
- 真实角色:被代理的角色
- 代理角色:代理真实角色,代理真实角色后,一般会做一些附属操作
- 客户:访问代理对象的人
代码步骤:
接口
public interface Rent { public void rent(); }
真实角色
public class Host implements Rent { @Override public void rent() { System.out.println("房东要出租房子。"); } }
代理角色
public class Proxy { private Host host; public Proxy(){ } public Proxy(Host host) { this.host = host; } public void rent(){ seeHouse(); host.rent(); doSome(); fee(); } //看房 public void seeHouse(){ System.out.println("中介带你看房。"); } //签合同 public void doSome(){ System.out.println("签约租赁合同。"); } //收中介费 public void fee(){ System.out.println("收中介费。"); } }
客户端访问代理角色
public class Client { public static void main(String[] args) { //房东要出租房子 Host host = new Host(); //代理,中介帮房东出租房子,代理一般会有一些附属操作 Proxy proxy = new Proxy(host); //租客不用面对房东,直接找中介租房即可。 proxy.rent(); } }
代理模式的好处:
- 可以使真实角色的操作更加纯粹,不用关注一些公共的业务
- 公共业务交给了代理角色,实现类业务的分工
- 公共业务发生扩展的时候,方便集中管理
代理模式的缺点:
- 一个真实角色就会产生一个代理角色,代码量会翻倍,开发效率会变低
接口:
public interface UserService {
public void add();
public void delete();
public void update();
public void query();
}
真实角色:
public class UserServiceImpl implements UserService {
@Override
public void add() {
System.out.println("增添了一位用户。");
}
@Override
public void delete() {
System.out.println("删除了一位用户。");
}
@Override
public void update() {
System.out.println("修改了一位用户。");
}
@Override
public void query() {
System.out.println("查询了一位用户。");
}
}
代理角色:
public class UserServiceProxy implements UserService {
private UserServiceImpl userService;
public void setUserService(UserServiceImpl userService) {
this.userService = userService;
}
@Override
public void add() {
log("add");
userService.add();
}
@Override
public void delete() {
log("delete");
userService.delete();
}
@Override
public void update() {
log("update");
userService.update();
}
@Override
public void query() {
log("query");
userService.query();
}
//日志方法
public void log(String msg){
System.out.println("使用了"+msg+"方法!");
}
}
访问代理:
public class Client {
public static void main(String[] args) {
UserServiceImpl userService = new UserServiceImpl();
UserServiceProxy proxy = new UserServiceProxy();
proxy.setUserService(userService);
proxy.delete();
}
}
10-2、动态代理
动态代理是程序在整个运行过程中根本就不存在目标类的代理类,目标对象的代理对象只是由代理工具(不是真实定义的类)在程序运行时由JVM根据反射等机制动态生成的。代理对象与目标对象的代理关系在程序运行时才确定。
- 动态代理和静态代理角色一样
- 动态代理的代理类是动态生成的,不是直接写好的
- 动态代理分为:基于接口的动态代理,基于类的动态代理
- 基于接口——JDK动态代理(要求目标对象必须实现接口,这是Java设计上的要求)
- 基于类:cglib(cglib代理的生成原理时生成目标类的子类,而子类时增强过的,这个子类对象就是代理对象。所以,使用cglib生成动态代理,要求目标类必须能够被继承,即不能时final的类。)
- java字节码实现:javassist
Proxy:代理,InvocationHandler:调用处理程序
动态代理的好处:
- 可以使真实角色的操作更加纯粹,不用关注一些公共的业务
- 公共业务交给了代理角色,实现类业务的分工
- 公共业务发生扩展的时候,方便集中管理
- 一个动态代理类代理的是一个接口,一般就是对应的一类业务
- 一个动态代理类可以代理多个类,只要是实现了同一个接口即可
十一、AOP
11-1、什么是AOP
AOP(Aspect Oriented Programming)面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
怎样理解面向切面编程?
1)需要在分析项目功能时,找出切面
2)合理的安排切面的执行时间(在目标方法前,还是目标方法后)
3)合理的安排切面执行的位置,在哪个类,哪个方法增加增强功能
11-2、AOP在Spring中的作用
提供声明式事务;允许用户自定义切面。
- 横切关注点:跨越应用程序多个模块的方法或功能。即是,与业务逻辑无关的,但是需要关注的部分,就是横切关注点。如日志,安全,缓存,事务等等……
- 切面(Aspect):横切关注点别模块化的特殊对象。即,它是一个类。
- 通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。
- 目标对象(Target):被通知对象。
- 代理(Proxy):向目标对象应用通知之后创建的对象。
- 切入点(PointCut):切面通知执行的“地点”的定义。
- 连接点(JointPoint):与切入点匹配的执行点。
SpringAOP中,通过Advice定义横切逻辑,Spring中支持5种类型的Advice
通知类型 | 连接点 | 实现接口 |
---|---|---|
前置通知 | 方法前 | org.springframework.aop.MethodBeforeAdvice |
后置通知 | 方法后 | org.springframework.aop.AfterReturningAdvice |
环绕通知 | 方法前后 | org.aopalliance.intercept.MethodInterceptor |
异常抛出通知 | 方法抛出异常 | org.springframework.aop.ThrowsAdvice |
引介通知 | 类中增加新的方法属性 | org.springframework.aop.IntroductionInterceptor |
即AOP在不改变原有代码的情况下,去增加新的功能。
一个切面有三个关键的要素:
1)切面的功能代码,切面能干什么
2)切面的执行位置,使用Pointcut表示切面执行的位置
3)切面的执行时间,使用Advice表示时间,在目标方法之前,还是目标方法之后
11-3、AOP的实现
spring在内部实现aop规范,能做aop的工作。
spring主要在事务处理时使用aop。
在项目开发中很少使用spring的aop实现,因为spring的aop比较笨重。
在Spring中使用AOP开发时,一般使用AspectJ的实现方式。Spring框架中结成了aspectj框架,通过spring就能使用aspectj的功能。
aspectJ:一个开源的专门做aop的框架,是一个优秀面向切面的框架,它扩展了Java语言,提供了强大的切面实现。
AspectJ是Eclipse的开源项目,官网地址:http://www.eclipse.org/aspectj/
aspectJ框架实现aop有两种方式:
- 使用xml的配置文件:配置全局事务
- 使用注解,在项目中要做aop功能,一般都使用注解,aspectj有5个注解
使用AOP织入,需要导入一个依赖包
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
方式一:使用Spring的API接口【主要SpringAPI接口实现】
方式二:自定义实现AOP【主要是切面定义】
方式三:使用注解实现
11-4、aspectj框架的使用
AspectJ的切入点表达式原型:
execution(modifiers-pattern? ret-type-pattern
declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
?
处表示可选,可没有。标红的是必须的。各部分之间用空格分开。
- modifiers-pattern:访问权限类型
- ret-type-pattern:返回值类型
- declaring-type-pattern:包名类名
- name-pattern(param-pattern):方法名(参数类型和参数个数)
- throws-pattern:抛出异常类型
以上表达式共4个部分:excution(访问权限 方法返回值 方法声明(参数) 异常类型)
在其中可以使用以下符号:
符号 | 意义 |
---|---|
* | 0至多个任意字符 |
.. | 使用在方法参数中,表示任意多个参数 用在包名后,表示当前包及其子包路径 |
+ | 用在类名后,表示当前类及其子类 用在接口后,表示当前接口及其实现类 |
例如:
execution(public ** (..)) 指定切入点为任意公共方法
execution(* set*(..)) 指定切入点为任何一个以“set”开始的方法
execution(* com.lskj.service.*.*(..)) 指定切入点为定义在service包里的任意类的任意方法
execution(* com.lskj.service..*.*(..)) 指定切入点定义在service包或者子包里的任意类的任意方法。“..”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类
execution(* *.service.*.*(..)) 指定只有一级包下的 serivce 子包下所有类(接口)中所有方法为切入点
execution(* *..service.*.*(..)) 指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点
execution(* *.ISomeService.*(..)) 指定只有一级包下的 ISomeSerivce 接口中所有方法为切入点
execution(* *..ISomeService.*(..)) 指定所有包下的 ISomeSerivce 接口中所有方法为切入点
execution(* com.xyz.service.IAccountService.*(..)) 指定切入点为:IAccountService 接口中的任意方法。
execution(* com.xyz.service.IAccountService+.*(..)) 指定切入点为:IAccountService 若为接口,则为接口中的任意方法及其所有实现类中的任意方法;若为类,则为该类及其子类中的任意方法。
execution(* joke(String,int))) 指定切入点为:所有的 joke(String,int)方法,且 joke()方法的第一个参数是 String,第二个参数是 int。如果方法中的参数类型是 java.lang 包下的类,可以直接使用类名,否则必须使用全限定类名,如 joke( java.util.List, int)。
execution(* joke(String,*))) 指定切入点为:所有的 joke()方法,该方法第一个参数为 String,第二个参数可以是任意类型,如joke(String s1,String s2)和joke(String s1,double d2)都是,但joke(String s1,double d2,Strings3)不是。
execution(* joke(String,..))) 指定切入点为:所有的 joke()方法,该方法第一个参数为 String,后面可以有任意个参数且参数类型不限,如 joke(String s1)、joke(String s1,String s2)和 joke(String s1,double d2,String s3) 都是。
execution(* joke(Object)) 指定切入点为:所有的 joke()方法,方法拥有一个参数,且参数是 Object 类型。joke(Object ob)是,但,joke(String s)与 joke(User u)均不是。
execution(* joke(Object+))) 指定切入点为:所有的 joke()方法,方法拥有一个参数,且参数是 Object 类型或该类的子类。不仅 joke(Object ob)是,joke(String s)和 joke(User u)也是。
1、切面的执行时间,这个执行时间在规范中叫做Advice(通知,增强)
在aspectj框架中使用注解表示,也可以使用xml配置文件中的标签
@Before
:前置通知在目标方法执行之前执行。被注解为前置通知的方法,可以包含一个 JoinPoint 类型参数。该类型的对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、目标对象等。
不光前置通知的方法,可以包含一个 JoinPoint 类型参数,所有的通知方法均可包含该参数。但是JoinPoint参数必须放在首位
@AfterReturning
:后置通知在目标方法执行之后执行。由于是目标方法之后执行,所以可以获取到目标方法的返回值。该注解的 returning 属性就是用于指定接收方法返回值的变量名的。所以,被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值的变量。该变量最好为 Object 类型,因为目标方法的返回值可能是任何类型。
@Around
:在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回值,Object 类型。并且方法可以包含一个ProceedingJoinPoint 类型的参数。接口 ProceedingJoinPoint 其有一个proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行。
@AfterThrowing
:异常通知在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象。
被注解为异常通知的方法可以包含一个参数 Throwable,参数名称为 throwing 指定的名称,表示发生的异常对象。
@After
:最终通知无论目标方法是否抛出异常,该增强均会被执行。
@Pointcut
:定义切入点(不是通知注解,并不表示切面的执行时间,是一个辅助的功能注解)当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。
其用法是,将@Pointcut 注解在一个方法之上,以后所有的 executeion 的 value 属性值均可使用该方法名作为切入点。代表的就是@Pointcut 定义的切入点。这个使用@Pointcut注解的方法一般使用 private 的标识方法,即没有实际作用的方法。
2、表示切面执行的位置,使用的是切入点表达式
使用AOP的目的是给已经存在的一些类和方法,增加额外的功能。前提是不改变原来的类的代码。
使用aspectj实现aop的基本步骤:
1、新建maven项目
2、加入依赖
1)spring依赖
2)aspectj依赖
3)junit单元测试
3、创建目标类:接口和它的实现类
需要做的是给类中的方法增加功能。
4、创建切面类:普通类
1)在类的上面加入@Aspect
2)在类中定义方法,方法就是切面要执行的功能代码
在方法的上面加入aspectj中的通知注解,例如@Before
还需要指定切入点表达式execution()
5、创建spring的配置文件:声明对象,把对象交给容器统一管理
声明对象可以使用注解,或xml配置文件
1)声明目标对象
2)声明切面类对象
3)声明aspectj框架中的自动代理生成器标签
自动代理生成器:用来完成代理对象的自动创建功能的。
6、创建测试类,从spring容器中获取目标对象(实际就是代理对象)
通过代理执行方法,实现aop的功能增强
11-5、测试
1、新建一个maven项目,aop-aspectj
2、导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lskj</groupId>
<artifactId>aop-aspectj</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--spring依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<!--aspectj依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
</dependencies>
<build>
</build>
</project>
3、创建目标类接口和其实现类。
方便测试,创建了一个实体类User
User.java
package com.lskj.pojo;
/**
* @author lskj
*/
public class User {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
SomeService.java
package com.lskj.service;
import com.lskj.pojo.User;
/**
* @author lskj
*/
public interface SomeService {
void doSome(String name,Integer age);
String doOther(String name,Integer age);
User doUser(String name,Integer age);
String doFirst(String name,Integer age);
void doSecond();
void doFinal();
}
SomeServiceImpl.java
package com.lskj.service.impl;
import com.lskj.pojo.User;
import com.lskj.service.SomeService;
/**
* @author lskj
*/
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
//给doSome方法增加一个功能,在doSome()执行之前,输出方法的执行时间
System.out.println("-----目标方法doSome()-----");
}
@Override
public String doOther(String name, Integer age) {
System.out.println("-----目标方法doOther()-----");
return "doOther";
}
@Override
public User doUser(String name,Integer age) {
System.out.println("-----目标方法doUser()-----");
User user = new User();
user.setId(1);
user.setName("user1");
return user;
}
@Override
public String doFirst(String name, Integer age) {
System.out.println("-----目标方法doFirst()-----");
return "doFirst";
}
@Override
public void doSecond() {
System.out.println("-----目标方法doSecond()-----");
int i = 1/0;
}
@Override
public void doFinal() {
System.out.println("-----目标方法doFinal()-----");
int i = 1/0;
}
}
4、创建切面类,增加功能
MyAspect.java
package com.lskj;
import com.lskj.pojo.User;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import java.util.Date;
/**
* @author lskj
*/
/**
* @Aspect:是aspectj框架中的注解
* 作用:表示当前类是切面类
* 切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
* 位置:在类定义的上面
*/
@Aspect
public class MyAspect {
/**
* 前置通知定义方法,方法是实现切面功能的
* 方法的定义要求:
* 1、公共方法public
* 2、方法没有返回值
* 3、方法名称自定义
* 4、方法可以有参数,也可以没参数。
* 如果有参数,参数不是自定义的,有几个参数类型可以使用
*/
/**
* @Before:前置通知注解
* 属性:value,是切入点表达式,表示切面的功能执行的位置
* 位置:在方法的上面
* 特点:
* 1、在目标方法之前先执行的
* 2、不会改变目标方法的执行结果
* 3、不会影响目标方法的执行
*/
@Before(value = "execution(public void com.lskj.service.impl.SomeServiceImpl.doSome(String,Integer))")
public void myBefore(){
//切面需要执行的代码
System.out.println("前置通知,切面功能:在目标方法之前输出执行时间:" + new Date());
}
/**
* 指定通知方法中的参数:JoinPoint
* JoinPoint:业务方法,要加入切面功能的业务方法
* 作用:可以在通知方法中获取方法执行时的信息,例如方法名称,方法的是实参。
* 如果切面功能中需要用到方法的信息,就加入JoinPoint
* 这个JoinPoint参数的值是由框架赋予,必须是第一个位置的参数
*/
@Before(value = "execution(public void com.lskj.service.impl.SomeServiceImpl.doSome(String,Integer))")
public void twoBefore(JoinPoint joinPoint){
//获取方法的完成定义
System.out.println("方法的签名(定义) = " + joinPoint.getSignature());
System.out.println("方法的名称 = " + joinPoint.getSignature().getName());
//获取方法的实参
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
System.out.println("参数 = " + arg);
}
//切面需要执行的代码
System.out.println("前置通知,切面功能:在目标方法之前输出执行时间:" + new Date());
}
/*---------------------------------------------------------------------------------------*/
/**
* 后置通知定义方法,方法是实现切面功能的
* 方法的定义要求:
* 1、公共方法public
* 2、方法没有返回值
* 3、方法名称自定义
* 4、方法有参数,推荐是Object,参数名自定义
*/
/**
* @AfterReturening:后置通知
* 属性:
* 1、value 切入点表达式
* 2、returning 自定义的变量,表示目标方法的返回值的
* 自定义变量名必须和通知方法的形参名一样
* 位置:在方法定义的上面
* 特点:
* 1、在目标方法之后执行
* 2、能够获取到目标方法的返回值,可以根据这个返回值做不同的处理功能
* Object obj = doOther();
* 3、可以修改这个返回值
* 后置通知的执行
* Object obj = doOther();
* 参数传递:传值,传引用
* myAfterReturning(obj);
* System.out.println("obj = " + obj);
*/
@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",returning = "obj")
public void myAfterReturning(Object obj){
//Object obj:是目标方法执行后的返回值,根据返回值做切面的功能处理
System.out.println("后置通知:在目标方法之后执行,获取的返回值是:"+obj);
if (obj.equals("doOther")){
System.out.println("返回值是doOther");
}else{
System.out.println("返回值不是doOther");
}
//修改目标方法的返回值,测试是否影响最后的方法调用结果
if (obj != null){
obj = "update后的值";
}
}
@AfterReturning(value = "execution(* *..SomeServiceImpl.doUser(..))",returning = "user")
public void twoAfterReturning(User user){
//Object obj:是目标方法执行后的返回值,根据返回值做切面的功能处理
System.out.println("后置通知:在目标方法之后执行,获取的返回值是:"+user);
//若修改了obj的内容,属性值,测试是否影响最后的调用结果
if (user != null){
user.setId(2);
user.setName("user2");
}
}
/*---------------------------------------------------------------------------------------*/
/**
* 环绕通知方法的定义格式
* 1、public
* 2、必须有一个返回值,推荐使用Object
* 3、方法名称自定义
* 4、方法有参数,固定的参数ProceedingJoinPoint
*
* 环绕通知:经常做事务,在目标方法之前开启事务,执行目标方法,在目标方法之后提交事务
*/
/**
* @Around:环绕通知
* 属性:value 切入点表达式
* 位置:在方法的定义上面
* 特点:
* 1、它是功能最强的通知
* 2、在目标方法的前和后都能增强功能
* 3、控制目标方法是否被调用执行
* 4、修改原来的目标方法的执行结果,影响最后的调用结果
*
* 环绕通知,等同于jdk动态代理的InvocationHandler接口
*
* 参数:ProceedingJoinPoint等同于Method
* 作用:执行目标方法的
* 返回值:就是目标方法的执行结果,可以被修改
*/
@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
String name = "";
//获取第一个参数值
Object[] args = pjp.getArgs();
if (args != null && args.length > 1){
Object arg = args[0];
name = (String)arg;
}
//实现环绕通知
Object obj = null;
System.out.println("环绕通知:在目标方法之前---");
//1、目标方法调用
if ("test04".equals(name)){
//符合条件,调用目标方法
obj = pjp.proceed(); //method.invoke(); Object obj = doFirst();
}
System.out.println("环绕通知:在目标方法之后,提交事务");
//2、在目标方法的前或后加入功能
//修改目标方法的执行结果,影响方法最后的调用结果
if (obj != null){
obj = "update doFirst";
}
//返回目标方法的执行结果
return obj;
}
/*---------------------------------------------------------------------------------------*/
/**
* 异常通知方法的定义要求:
* 1、公共方法public
* 2、方法没有返回值
* 3、方法名称自定义
* 4、方法有一个Exception参数,如果还有,则是JoinPoint
*/
/**
* @AfterThrowing:异常通知
* 属性:1、value 切入点表达式
* 2、throwing 自定义的变量,表示目标方法抛出的异常对象
* 变量名必须和方法的参数名一样
* 特点:
* 1、在目标方法抛出异常时执行
* 2、可以做异常的监控程序,监控目标方法执行时是否有异常
* 若有异常,可以发送邮件、短信进行通知
*
* 执行:
* try{
* SomeServiceImpl.doSecond(..)
* }catch(Exception e){
* myAfterThrowing(e);
* }
*/
@AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))",throwing = "e")
public void myAfterThrowing(Exception e){
System.out.println("异常通知:方法发生异常时,执行:" + e.getMessage());
//发送邮件、短信,通知开发人员
}
/*---------------------------------------------------------------------------------------*/
/**
* 最终通知方法的定义要求:
* 1、公共方法public
* 2、没有返回值
* 3、方法名称自定义
* 4、方法没有参数,如果有,则是JoinPoint
*/
/**
* @After:最终通知
* 属性:value 切入点表达式
* 位置:在方法的上面
* 特点:
* 1、总是会执行
* 2、在目标方法之后执行的
*
* try{
* SomeServiceImpl.doFinal(..)
* }catch(Exception e){
*
* }finally{
* myAfter()
* }
*/
@After(value = "execution(* *..SomeServiceImpl.doFinal(..))")
public void myAfter(){
System.out.println("执行最终通知,总是会被执行的代码");
//一般做资源清除功能
}
/*---------------------------------------------------------------------------------------*/
/**
* @Pointcut:定义和管理切入点,若项目中有多个切入点表达式是重复的,可以复用的,使用@Pointcut
* 属性:value 切入点表达式
* 位置:在自定义的方法上面
* 特点:
* 当使用@Pointcut定义在一个方法的上面,此时这个方法的名称就是切入点表达式的别名。
* 其它的通知中,value属性就可以使用这个方法名称,代替切入点表达式了
*/
@Pointcut(value = "execution(* *..SomeServiceImpl.doSome(..))")
public void myPt(){
//不需要写代码
}
@After(value = "myPt()")
public void pt(){
System.out.println("执行后置通知");
}
}
5、创建spring的配置文件:声明对象,把对象交给容器统一管理
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--把对象交给spring容器,由spring容器统一创建,管理对象-->
<!--声明目标对象-->
<bean id="someService" class="com.lskj.service.impl.SomeServiceImpl"/>
<!--声明切面类对象-->
<bean id="myAspect" class="com.lskj.MyAspect"/>
<!--声明自动dialing生成器:使用aspectj框架内部的功能,创建目标对象的代理对象。
创建代理对象是在内存中实现的,修改目标对象的内存中的结构。创建为代理对象
所以目标对象就是被修改后的代理对象
aspectj-autoproxy:会把spring容器中的所有的目标对象,一次性都生成代理对象
-->
<aop:aspectj-autoproxy />
</beans>
6、创建测试类,从spring容器中获取目标对象进行测试
package com.lskj;
import com.lskj.pojo.User;
import com.lskj.service.SomeService;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author lskj
*/
public class MyTest {
String config = "applicationContext.xml";
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(config);
//从容器中获取目标对象
SomeService proxy = (SomeService) context.getBean("someService");
@Test
public void test01(){
//通过代理的对象执行方法,实现目标方法执行时,增强了功能
proxy.doSome("test01",20);
}
@Test
public void test02(){
String str = proxy.doOther("test02", 21);
System.out.println("str = "+str);
}
@Test
public void test03(){
User user2 = proxy.doUser("user2",2);
System.out.println(user2);
}
@Test
public void test04(){
String test04 = proxy.doFirst("test04", 22);
System.out.println("返回值 = "+test04);
}
@Test
public void test05(){
proxy.doSecond();
}
@Test
public void test06(){
proxy.doFinal();
}
@Test
public void test07(){
proxy.doSome("test07",1);
}
}
测试结果
test01:
前置通知,切面功能:在目标方法之前输出执行时间:xxx
方法的签名(定义) = void com.lskj.service.SomeService.doSome(String,Integer)
方法的名称 = doSome
参数 = test01
参数 = 20
前置通知,切面功能:在目标方法之前输出执行时间:xxx
-----目标方法doSome()-----
test02:
-----目标方法doOther()-----
后置通知:在目标方法之后执行,获取的返回值是:doOther
返回值是doOther
str = doOther
test03:
-----目标方法doUser()-----
后置通知:在目标方法之后执行,获取的返回值是:User{id=1, name='user1'}
User{id=2, name='user2'}
test04:
环绕通知:在目标方法之前---
-----目标方法doFirst()-----
环绕通知:在目标方法之后,提交事务
返回值 = update doFirst
test05:
-----目标方法doSecond()-----
异常通知:方法发生异常时,执行:/ by zero
test06:
-----目标方法doFinal()-----
执行最终通知,总是会被执行的代码
test07:
前置通知,切面功能:在目标方法之前输出执行时间:xxx
方法的签名(定义) = void com.lskj.service.SomeService.doSome(String,Integer)
方法的名称 = doSome
参数 = test07
参数 = 1
前置通知,切面功能:在目标方法之前输出执行时间:xxx
-----目标方法doSome()-----
执行后置通知
若目标类是接口和其实现类,则使用的是jdk的动态代理。
若目标类没有接口,则使用的是cglib动态代理,spring框架会自动应用cglib。
若希望目标类有接口和其实现类,但是想要使用cglib动态代理,需要在applicationContext.xml中增加
proxy-target-class="true"
如下<aop:aspectj-autoproxy proxy-target-class="true"/>
十二、整合Mybatis
把mybatis框架和spring集成在一起,使用的是IOC。
因为IOC可以创建对象,可以把mybtis框架中的对象交给spring统一创建,开发人员从spring中获取对象,不需要同时面对两个或多个框架了,只需要面对一个spring框架。
Mybatis使用步骤:
1、定义dao(mapper)接口
2、定义mapper文件
3、定义mybatis的主配置文件mybatis.xml
4、创建dao(mapper)的代理对象
要使用dao对象,需要使用getMapper()方法,怎么能使用getMapper()方法,需要哪些条件?
1、获取SqlSession对象,需要使用SqlSessionFactory的openSession()方法。
2、创建SqlSessionFactory对象,通过读取mybatis的主配置文件,能创建SqlSessionFactory对象
需要SqlSessionFactory对象,使用Factory能获取SqlSession,有了SqlSession就能有dao,目的就是获取dao对象。
Factory创建需要读取主配置文件。
主配置文件:1、数据库信息 2、mapper文件位置
综上,需要让spring创建以下对象:
- 独立的连接池类的对象,使用阿里的druid连接池
- SqlSessionFactory对象
- 创建出dao对象
步骤:
新建maven项目,导入相关jar包
- junit
- spring
- mybatis
- mysql驱动
- spring的事务的依赖
- mybatis-spring【new】
创建实体类
创建dao接口和mapper文件
创建mybtis配置文件
创建Service接口和实现类
创建spring的配置文件:声明mybatis的对象交给spring创建
- 数据源
- SqlSessionFactory
- Dao对象
- 声明自定义的service
<?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"> <!-- 把数据库的配置信息,写在一个独立的文件,编译修改数据库的配置内容 spirng知道jdbc.properties文件的位置 --> <context:property-placeholder location="classpath:jdbc.properties"/> <!--声明数据源DataSource,作用是连接数据库的--> <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!--set注入给DruidDataSource提供连接数据库信息--> <!--使用属性配置文件中的数据,语法 ${key}--> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="maxActive" value="30"/> </bean> <!--声明的是mybatis中提供SqlSessionFactoryBean类,这个类内部创建SqlSessionFactory的 SqlSessionFactory sqlSessionFactory = new .. --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!--set注入,把数据库连接池赋给dataSource属性--> <property name="dataSource" ref="myDataSource"/> <!--mybatis主配置文件位置 configLocation属性是Resource类型,读取配置文件 它的赋值,使用value,指定文件的路径,使用classpath:表示文件的位置 --> <property name="configLocation" value="classpath:mybatis.xml"/> </bean> <!-- 创建dao对象,使用SqlSession的getMapper(xxxDao.class) MapperScannerConfigurer:在内部调用getMapper()生成每个dao接口的代理对象 --> <bean class="org.mybatis.spirng.mapper.MapperScannerConfigurer"> <!-- 指定包名,包名是dao接口所在的包名 MapperScannerConfigurer会扫描这个包中的所有接口,把每个接口都执行 一次getMapper()方法,得到每个接口的dao对象 创建好的dao对象放入到spring的容器中。dao对象的默认名称是接口名首字母小写 --> <property name="basePackage" value="com.xxx.dao"/> </bean> <!--声明service--> <bean id="xxxService" class="com.xxx.service.impl.xxxServiceImpl"> <property name="xxxDao" ref="xxxDao"/> </bean> </beans>
创建测试类,获取Service对象,通过service调用dao完成对数据库的访问
12-1、mybatis
- 编写实体类
- 编写核心配置文件
- 编写接口
- 编写Mapper.xml
- 测试
12-2、Mybatis-Spring
- 编写数据源配置
- sqlSessionFactory
- sqlSessionTemplate
- 需要给接口加实现类
- 将实现类注入到Spring中
- 测试
十三、声明式事务
声明式事务:把事务相关的资源和内容都提供给spring,spring就能处理事务提交、回滚。几乎不用写代码。
13-1、事务
- 把一组业务当成一个业务来做;要么都成功,要么都失败。
- 确保完整性和一致性
事务ACID原则:
- 原子性
- 一致性
- 隔离性
- 多个业务可能操作同一个资源,防止数据损坏
- 持久性
- 事务一旦提交,无论系统发生什么问题,结果都不会再被影响,被持久化写到存储器中。
13-2、Spring中的事务管理
spring提供一种处理事务的统一模型,能使用统一步骤、方式完成多种不同数据库访问技术的事务处理。
例如使用spring的事务处理机制,可以完成mybatis访问数据库的事务处理,也可以完成hibernate访问数据库的事务处理
- 声明式事务:AOP
- 编程式事务:在代码中进行事务的管理
事务原本是数据库中的概念,在Dao层。但一般情况下,需要将事务提升到业务层,即Service层。这样做是为了能够使用事务的特性来管理具体的业务。
在Spring中通常可以通过以下两种方式来实现对事务的管理:
- 使用spring的事务注解管理事务
- 使用AspectJ的AOP配置事务管理事务
为什么需要事务?
- 如果不配置事务,可能存在数据提交不一致的情况;
- 若不在Spring中配置声明式事务,就需要在代码中手动配置事务。
处理事务,需要怎样做?做什么?
spring处理事务的模型,使用的步骤都是固定的。把事务使用的信息提供给spring即可。
1、事务内部提交、回滚事务,使用的是事务管理器对象,代替你完成commit、rollback
事务管理器是一个接口和它的众多实现类
接口:PlatformTransactionManager,定义了事务重要方法commit、rollback
实现类:spring把每一种数据库访问技术对应的事务处理类创建好了。
- mybatis访问数据库:spring创建好的是DataSourceTransactionManager
- hibernate访问数据库:spring创建的是HibernateTransactionManager
怎么使用?怎样告诉spring你用的是哪种数据库访问技术呢?
声明数据库访问技术对应的事务管理器实现类,在spring的配置文件中使用
<bean>
声明即可。例如,使用mybatis访问数据库,在xml配置文件中配置
<bean id="xxx" class="...DataSourceTransactionManager"/>
2、说明事务的类型(业务方法需要什么样的事务)
说明方法需要的事务:
事务的隔离级别:有4个值
- DEFAULT:采用DB默认的事务隔离级别。Mysql的默认为REPEATABLE_READ,Oracle默认为READ_COMMITTED
- READ_UNCOMMITTED:读未提交。未解决任何并发问题。
- READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。
- REPEATABLE_READ:可重复读。解决脏读,不可重复读,存在幻读。
- SERIALIZABLE:串行化。不存在并发问题。
事务的超时时间:表示一个方法的最长的执行时间,如果方法执行时超过了时间,事务就会回滚。
单位是秒,整数值,默认是-1
事务的传播行为:控制业务方法是不是有事务的,是什么样的事务。
7个传播行为,表示你的业务方法调用时,事务在方法之间时如何使用的。
PROPAGATION_REQUIRED:指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。这种传播行为是最常见的选择,也是Spring默认的事务传播行为。
PROPAGATION_REQUIRES_NEW:总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。
PROPAGATION_SUPPORTS:指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。
PROPAGATION_MANDATORY
PROPAGATION_NESTED
PROPAGATION_NEVER
PROPAGATION_NOT_SUPPORTED
3、spring提交事务,回滚事务的时机
当你的业务方法,执行成功,没有异常抛出,当方法法执行完毕,spring在方法执行后提交事务。
当你的业务方法抛出运行时异常,spring执行回滚,调用事务管理器的rollback
运行时异常的定义:RuntimeException和它的子类都是运行时异常,例如NullPointException,NumberFormatException
当你的业务方法抛出非运行时异常,主要时受查异常时,提交事务。
- 受查异常:编写代码时,必须处理的异常。例如IOException,SQLException
总结:
1、管理事务的是事务管理器和它的实现类
2、spring的事务是一个统一模型
- 指定要使用的事务管理器实现类,使用
<bean>
- 指定哪些类,哪些方法需要加入事务的功能
- 指定方法需要的隔离级别、传播行为、超时
13-3、使用Spring的事务注解管理事务
适合中小型项目。
Spring框架自己用aop实现给业务方法增加事务的功能,使用@Transactional
注解增加事务。
@Transactional
注解是Spring框架的注解,放在public方法的上面,表示当前方法具有事务。可以给注解的属性赋值,表示具体的隔离级别、传播行为、异常信息等等。
@Transactional
注解的所有可选属性如下:
- propagation:用于设置事务传播属性。该属性类型为Propagation枚举,默认值为Propagation.REQUIRED。
- isolation:用于设置事务的隔离级别。该属性类型为Isolation枚举,默认值为Isolation.DEFAULT。
- readOnly:用于设置该方法对数据库的操作是否是只读的。该属性为boolean,默认值为false。
- timeout:用于设置本操作与数据库连接的超时时限。单位为秒,类型为int,默认值为-1,即没有时限。
- rollbackFor:指定需要回滚的异常类。类型为Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
- spring框架会首先检查方法抛出的异常是否在rollbackFor的属性值中。若异常在rollbackFor列表中,无论是什么样类型的异常,一定回滚。
- 若抛出的异常不再rollbackFor列表中,spring会判断异常是否是RuntimeException,如果是,一定回滚。
- rollbackForClassName:指定需要回滚的异常类类名。类型为String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
- noRollbackFor:指定不需要回滚的异常类。类型为Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
- noRollbackForClassName:指定不需要回滚的异常类类名。类型为String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
需要注意的是,@Transactional若用在方法上,只能用于public方法上。对于其他非public方法,如果加上了注解@Transactional,虽然Spring不会报错,但不会将指定事务织入到该方法中。因为Spring会忽略掉所有非public方法上的@Transaction注解。
若@Transaction注解在类上,则表示该类上所有的方法均将在执行时织入事务。
使用@Transactional的步骤:
1、需要声明事务管理器对象。
<!--声明事务管理器对象--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="声明的数据源DataSource的id"/> </bean>
2、开启事务注解驱动,告诉Spring框架需要使用注解的方式管理事务。
<!--声明事务注解驱动--> <tx:annotation-driven transction-manager="transationManager"/>
——————————————————————
spring使用aop机制,创建@Transactional所在的类代理对象,给方法加入事务的功能。spring给业务加入事务,在业务方法执行前,先开启事务,在业务方法之后提交或回滚事务,使用了aop的环绕通知@Around("需要增加的事务功能的业务方法名称") Object myAround(){ 开启事务,spring开启 try{ spring的事务管理.commit(); }catch(Exception e){ spring的事务管理.rollback(); } }
——————————————————————
3、在方法的上面加入@Transactional注解
13-4、使用AspectJ的AOP配置管理事务
适合大型项目,这种方式业务方法和事务配置完全分离。
使用XML配置事务代理的方式的不足是,每个目标类都需要配置事务代理。当目标类较多,配置文件会变得非常臃肿。
实现步骤:都是在xml配置文件中实现。
1、加入aspectj依赖。
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
2、声明事务管理器对象。
<!--声明事务管理器对象-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="声明的数据源DataSource的id"/>
</bean>
3、声明方法需要的事务类型(配置方法的事务属性:隔离级别、传播行为、超时)。
使用tx标签必须引入约束。
<!--声明业务方法的事务属性
id:自定义名称,表示<tx:advice>和</tx:advice>之间的配置内容
transaction-manager:事务管理对象的id
-->
<tx:advice id="xxxAdvice" transaction-manager="transactionManager">
<!--tx:attributes:配置事务属性-->
<tx:attributes>
<!--tx:method:给具体的方法配置事务属性,method可以有多个,分别给不同的方法设置事务属性
name:方法名称
1、完成的方法名称,不带有包和类
2、方法可以使用通配符,*表示任意字符
propagation:传播行为,枚举值
isolation:隔离级别
rollback-for:指定的异常类名,全限定类名。发生异常一定回滚
-->
<tx:method name="方法名" propagation="REQUIRED" isolation="DEFAULT" rollback-for="java.lang.NullPointerException,xxx.xxx.xxx"/>
<tx:method name="*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
4、配置aop:指定哪些类要创建代理。
<!--配置aop-->
<aop:config>
<!--配置切入点表达式:指定哪些包中的类,需要使用事务
id:切入点表达式的名称,唯一值
expression:切入点表达式,指定哪些类要使用事务,aspectj会创建代理对象
-->
<aop:pointcut id="xxxx" expression="execution(* *..service..*.*(..))"/>
<!--配置增强器:关联advice和pointcut
advice-ref:通知,上面tx:advice那里的配置
pointcut-ref:切入点表达式的id
-->
<aop:advisor advice-ref="xxxAdvice" pointcut-ref="xxxx"/>
</aop:config>