DI 注入注解
因为之前 XML 配置方式比较繁琐。
<bean id="person" class="cn.XXX._01_hello.Person">
<property name="name" value="大罗"/>
</bean>
注入注解分两类,Spring 的 Autowire,JavaEE 的 Resource,两者作用是一样,完成属性或字段的注入,注入是 bean(取代 XML property ref 元素)。而 Spring 的 Value 注解也是完成属性或字段的注入,注入是常量值(取代 XML property value 元素)。
Autowired 和 Value注解使用
编写类
public class Dog {
@Value("黄色")
private String color;
@Override
public String toString() {
return "Dog [color=" + color + "]";
}
}
public class Person {
@Autowired
private Dog dog;
@Override
public String toString() {
return "Person [dog=" + dog + "]";
}
}
编写配置文件
<bean id="dog" class="cn.xxx._04_anno.Dog"/>
<bean id="person" class="cn.xxx._04_anno.Person"/>
<context:annotation-config/>
Autowired 注解细节
- 可以让 Spring 自动的把属性或字段需要的对象找出来,并注入到属性上或字段上。
- 可以贴在字段或者 setter 方法上面。
- 可以同时注入多个对象。
- 可以注入一些 Spring 内置的重要对象,比如 BeanFactory,ApplicationContext 等。
- 默认情况下 Autowired 注解必须要能找到对应的对象,否则报错。通过 required=false 来避免这个问题:@Autowired(required=false),会给该字段赋值为null
- 第三方程序:Spring3.0 之前需要手动配置 Autowired 注解的解析程序:< context:annotation-config/>,在 Spring 的测试环境可以不配置,在 Web 开发中换一种配置< context:component-scan base-package=“cn.xxx.扫描的包路径”/> 解决。
- Autowired 注解寻找 bean 的方式:
- 首先按照依赖对象的类型找,若找到,就是用 setter 或者字段直接注入。
- 如果在 Spring 上下文中找到多个匹配的类型,再按照名字去找,若没有匹配报错。
- 可以通过加一个注解 @Qualifier(“xml配置bean的id”) 标签来规定依赖对象按照 bean 的 id 和 类型的组合方式去找。
Resource 注解使用
添加依赖
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
使用
修改上面代码的 Person 类使用 Resource 注解,再重新测试即可。
@Component
public class Person {
@Resource
private Dog dog;
@Override
public String toString() {
return "Person [dog=" + dog + "]";
}
}
Resource 注解细节
- 可以让 Spring 自动的把属性或字段需要的对象找出来,并注入到属性上或字段上。
- 可以贴在字段或者 setter 方法上面。
- 可以注入一些 Spring 内置的重要对象,比如 BeanFactory,ApplicationContext 等。
- Resource 注解必须要能找到对应的对象,否则报错。
- 第三方程序:Spring3.0 之前需要手动配置 Autowired 注解的解析程序:
<context:annotation-config/> ,在 Spring 的测试环境可以不配置, Web 开发中换一种配置解决。 - Resource 注解寻找 bean 的方式:
- 首先按照名字去找,如果找到,就使用 setter 或者字段注入。
- 若按照名字找不到,再按照类型去找,但如果找到多个匹配类型,报错。
- 可以直接使用 name 属性指定 bean 的名称(@Resource(name=“”));但若指定的 name,就只能按照 name 去找,若找不到,就不会再按照类型去。
IoC 注解
XML 配置问题
Spring 的 XML 配置文件中还有很多 bean 的配置,那能不能让 Spring 通过什么方式来简化配置呢?
<bean id="dog1" class="cn.xxx._04_anno.Dog">
<property name="color" value="黄色"/>
</bean>
<bean id="dog2" class="cn.xxx._04_anno.Dog">
<property name="color" value="黑色"/>
</bean>
IoC 注解
四个注解的功能是相同的,贴在类上 , 都是将某个类的对象交给spring管理 , 只是用于标注不同类型的类上:
- @Repository:用于标注数据访问组件,即 DAO 实现类上。
- @Service:用于标注业务层实现类上。
- @Controller:用于标注控制层类上(如 SpringMVC 的 Controller)。
- @Component:当不是以上的这几种分类含义的类,可以使用这个注解进行标注。
修改配置文件
<context:component-scan base-package="cn.xxx.扫描的包路径"/>
Scope 和 PostConstruct 以及 PreDestroy 注解
问题
比如之前使用 XML 方式配置 bean 的作用域等能不能也使用注解的方式?
注解说明
- @Scope:贴在类上,标明 bean 的作用域。
- @PostConstruct:贴在方法上,标明 bean 创建完后调用此方法。
- @PreDestroy:贴在方法上,标明容器销毁时调用此方法。
编写配置文件
配置文件内容不变,还是跟上面一样。
控制事务繁琐
问题
考虑一个应用场景:需要对系统中的某些业务方法做事务管理,拿简单的 save 操作举例。没有加上事务控制的代码如下
public class EmployeeServiceImpl implements IEmployeeService {
public void save(Employee employee){
}
}
修改源代码,加上事务控制之后:
public class EmployeeServiceImpl implements IEmployeeService {
public void save(Employee employee){
try {
}catch (Exception e){
}finally{
}
}
}
上述问题:在我们的业务层中每一个业务方法都得处理事务(繁琐的 try-catch),这样设计上存在两个很严重问题:
- 代码结构重复:在开发中不要重复代码,重复就意味着维护成本增大;
- 责任不分离:业务方法只需要关心如何完成该业务功能,不需要去关系事务管理、日志管理、权限管理等等。
租房案例
 问题:此时若有人来整房东,派很多人来找房东假租房,这会导致房东一天到晚都忙且没收获。带来这个问题就是:重复,且责任不分离,其实房东最关心的就是签合同和收房租。  后面我们就借鉴上面生活的例子的思想来之前事务繁琐的问题。
代理模式
好处
客户端(租客)直接使用的都是代理对象(中介),不知道真实对象(房东)是谁,此时代理对象(中介)可以在客户端(租客)和真实对象(房东)之间起到中介的作用。
- 代理对象完全包含真实对象(中介存着房东联系方式),客户端(租客)使用的都是代理对象(中介)的方法,和真实对象没有直接关系;
- 代理模式的职责:把不是真实对象(房东)该做的事情(比如看房子,谈价格等等)从真实对象(房东)上撇开—职责分离。
代理分类
- 静态代理:在程序运行前就已经存在代理类的字节码文件,代理对象和真实对象的关系在运行前就确定了。(即代理类及对象要我们自己创建)
- 动态代理:代理类是在程序运行期间由 JVM 通过反射等机制动态的生成的,所以不存在代理类的字节码文件,动态生成字节码对象,代理对象和真实对象的关系是在程序运行时期才确定的。(即代理类及对象不要我们自己创建)
动态代理实现方式
- 针对真实类有接口使用 JDK 动态代理;
- 针对真实类没实现接口使用 CGLIB 或 Javassist 组件。
动态代理实现机制
由于 JVM 通过字节码的二进制信息加载类的,若我们在运行期系统中,遵循 Java 编译系统组织 .class 文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据加载转换成对应的类。如此,就完成了在代码中动态创建一个类的能力了。 
静态代理
给业务类的方法增加模拟的事务。
类体系图

新建一个maven项目,设置编译版本及添加依赖
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.8.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
编写业务接口及实现
public interface IEmployeeService {
void save(String username, String password);
}
public class EmployeeServiceImpl implements IEmployeeService {
@Override
public void save(String username, String password) {
System.out.println("保存:" + username + ":" + password);
}
}
编写代理类
public class EmployeeServiceProxy implements IEmployeeService {
private IEmployeeService target;
public void setTarget(IEmployeeService target) {
this.target = target;
}
private MyTransactionManager tx;
public void setTx(MyTransactionManager tx) {
this.tx = tx;
}
@Override
public void save(String username, String password) {
try {
tx.begin();
target.save(username, password);
tx.commit();
}catch (Exception e) {
e.printStackTrace();
tx.rollback();
}
}
}
编写事务模拟类
public class MyTransactionManager {
public void begin() {
System.out.println("开启事务");
}
public void commit() {
System.out.println("提交事务");
}
public void rollback() {
System.out.println("回滚事务");
}
}
使用xml或注解配置
<bean id="tx" class="cn.wolfcode.tx.MyTransactionManager"/>
<bean id="employeeServiceProxy" class="cn.wolfcode.service.impl.EmployeeServiceProxy">
<property name="target">
<bean class="cn.wolfcode.service.impl.EmployeeServiceImpl"/>
</property>
<property name="tx" ref="tx"></property>
</bean>
优缺点
优点
- 业务类只需要关注业务逻辑本身,保证了业务类的重用性。
- 把真实对象隐藏起来了,保护真实对象(用了 Spring )。
缺点
- 代理对象的某个接口只服务于某一种类型的对象,也就是为每个真实类创建一个代理类,比如项目还有其他业务类呢。
- 若需要代理的方法很多,则要为每一种方法都进行代理处理。
- 若接口增加一个方法,除了所有实现类需要实现这个方法外,代理类也需要实现此方法。
JDK 动态代理
给业务类的方法增加模拟的事务。
java.lang.reflect.Proxy
Java 动态代理机制生成的所有动态代理类的父类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。 主要方法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler hanlder)
- 方法职责:根据指定的类加载器、一组需要代理的接口及方法调用处理器生成动态代理类实例。
- 参数:
- loader :类加载器,一般给真实对象的类加载器;
- interfaces:代理类需要实现的接口,代理这个接口中的所有方法;
- handler:调用处理器,就是生成的代理对象在执行方法时是执行这个对象里我们定义的调用规则。
- 返回:创建好的代理对象。
java.lang.reflect.InvocationHandler 调用处理器
主要方法:
public Object invoke(Object proxy, Method method, Object[] args)。
- 方法职责:负责处理动态代理类上的所有方法调用,让使用者自定义做什么事情,对原来方法增强(加什么功能)。
- 参数:
- proxy :生成的代理对象;
- method:当前需要代理的方法;
- args :当前调用方法的实参。
- 返回:代理逻辑和真实对象调用方法之后想要返回的结果。
JDK动态代理代码实现
编写 TransactionInvocationHandler 类
该类实现 InvocationHandler 接口,实现 invoke 方法,实现增强操作。
public class TransactionInvocationHandler implements InvocationHandler {
private Object target;
public void setTarget(Object target) {
this.target = target;
}
public Object getTarget() {
return target;
}
private MyTransactionManager tx;
public void setTx(MyTransactionManager tx) {
this.tx = tx;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("到此一游");
Object retVal = null;
try {
tx.begin();
retVal= method.invoke(target, args);
tx.commit();
}catch (Exception e) {
tx.rollback();
}
return retVal;
}
}
修改 applicationContext.xml
配置 TransactionInvocationHandler、MyTransactionManager、EmployeeServiceImpl,让 Spring 帮我们创建这些对象组装依赖,也可以不用spring帮我们进行管理。
<bean id="tx" class="cn.wolfcode.tx.MyTransactionManager"/>
<bean id="transactionInvocationHandler" class="cn.wolfcode.handler.TransactionInvocationHandler">
<property name="target">
<bean class="cn.wolfcode.service.impl.EmployeeServiceImpl"/>
</property>
<property name="tx" ref="tx"/>
</bean>
测试类
注入类型 InvocationHandler 的 bean,在测试方法中手动使用 Proxy 中的方法创建代理对象,调用代理对象的方法
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class EmployeeServiceTest {
@Autowired
private TransactionInvocationHandler transactionInvocationHandler;
@Test
public void testSave() {
IEmployeeService proxy = (IEmployeeService)Proxy.newProxyInstance(
transactionInvocationHandler.getTarget().getClass().getClassLoader(),
transactionInvocationHandler.getTarget().getClass().getInterfaces(),
transactionInvocationHandler);
proxy.save("罗老师", "666");
}
}
优缺点
优点
对比静态代理,发现不需手动地提供那么多代理类。
缺点
- 真实对象必需实现接口(JDK 动态代理特有);
- 动态代理的最小单位是类(类中方法都会被代理),如果只想拦截一部分方法,可以在 invoke 方法中对要执行的方法名进行判断;
- 对多个真实对象进行代理的话,要手动写代码调用api创建代理对象,用起来麻烦。
JDK 动态代理原理
获取代理类字节码文件
ProxyGenerator 在 JDK 11 已经不是公有的了。
通过反编译工具查看字节码文件
使用 IDEA 就可以反编译。观察:save 方法,发现底层其实依然在执行 InvocationHandler 中的 invoke 方法。 
CGLIB 动态代理
JDK 动态代理的问题
JDK 动态代理要求真实类必须实现接口。而 CGLIB 与 JDK 动态代理不同是,真实类不用实现接口,生成代理类的代码不一样且代理类会继承真实类。
org.springframework.cglib.proxy.Enhancer
类似 JDK 中 Proxy,用来生成代理类创建代理对象的。
org.springframework.cglib.proxy.InvocationHandler
类似 JDK 中 InvocationHandler,让使用者自定义做什么事情,对原来方法增强。
CGLIB 动态代理代码实现
改用 Enhancer API 来生成代理类创建代理对象。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class EmployeeServiceTest {
@Autowired
private TransactionInvocationHandler transactionInvocationHandler;
@Test
public void testSave() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(transactionInvocationHandler.getTarget().getClass());
enhancer.setCallback(transactionInvocationHandler);
EmployeeServiceImpl proxy = (EmployeeServiceImpl)enhancer.create();
proxy.save("罗老师", "666");
}
}
动态代理总结
JDK 动态代理总结
- Java 动态代理是使用 java.lang.reflect 包中的 Proxy 类与 InvocationHandler 接口这两个来完成的。
- 要使用 JDK 动态代理,真实类必须实现接口,并只能代理这个接口中的方法。
- JDK 动态代理将会拦截所有 pubic 的方法(因为只能调用接口中定义的方法),这样即使在接口中增加了新的方法,不用修改代码也会被拦截。
- 动态代理的最小单位是类(类中某些方法都会被处理),如果只想拦截一部分方法,可以在 invoke 方法中对要执行的方法名进行判断。
- 特征 : 代理类与真实类共同实现相同的接口。
CGLIB 动态代理总结
- CGLIB 可以生成真实类的子类,并重写父类非 final 修饰符的方法。
- 要求真实类不能是 final 的,要拦截的方法要是非 final、非 static、非 private 的。
- 动态代理的最小单位是类(类中某些方法都会被处理),如果只想拦截一部分方法,可以在 invoke 方法中对要执行的方法名进行判断。
- 特征 : 代理类是继承真实类。
两种代理的选用
JDK 动态代理是基于实现接口的,CGLIB 和 Javassit 是基于继承真实类的。
从性能上考虑:Javassit > CGLIB > JDK。
选用如下:
- 若真实类实现了接口,优先选用 JDK 动态代理。(因为会产生更加松耦合的系统,也更符合面向接口编程)
- 若真实类没有实现任何接口,使用 Javassit 和 CGLIB 动态代理。
|