Spring注解式开发详解
2024/1/6大约 7 分钟
Spring注解式开发详解
本章内容
本章将深入学习Spring的注解式开发,包括反射机制回顾、手写Spring框架实现原理,以及Spring IoC注解式开发的详细使用方法。
反射机制回顾
在学习Spring注解式开发之前,我们先回顾一下Java反射机制,这是Spring框架实现的核心技术之一。
反射机制的四要素
方法调用包含四个要素:
- 调用对象:哪个对象
- 调用方法:调用什么方法
- 传递参数:传递什么参数
- 返回结果:方法执行后的返回值
反射操作属性示例
通过反射机制给对象属性赋值:
package com.powernode.reflect;
import java.lang.reflect.Method;
public class UserTest {
public static void main(String[] args) throws Exception{
// 已知类名
String className = "com.powernode.reflect.User";
// 已知属性名
String propertyName = "age";
// 通过反射机制给User对象的age属性赋值20岁
Class<?> clazz = Class.forName(className);
Object obj = clazz.newInstance(); // 创建对象
// 根据属性名获取setter方法名
String setMethodName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
// 获取Method
Method setMethod = clazz.getDeclaredMethod(setMethodName, int.class);
// 调用Method
setMethod.invoke(obj, 20);
System.out.println(obj);
}
}
手写Spring框架
实现原理
Spring IoC容器的实现原理:工厂模式 + 解析XML + 反射机制
我们来手写一个简化版的Spring框架,命名为:myspring
第一步:创建项目结构
创建Maven项目,引入必要依赖:
pom.xml配置
<?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>org.myspringframework</groupId>
<artifactId>myspring</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<dependencies>
<!-- XML解析依赖 -->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.2.0</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
第二步:定义核心接口
package org.myspringframework.core;
/**
* 应用上下文接口
*/
public interface ApplicationContext {
/**
* 根据bean的id获取bean实例
* @param beanId bean的id
* @return bean实例
*/
Object getBean(String beanId);
}
第三步:实现核心容器类
ClassPathXmlApplicationContext实现
package org.myspringframework.core;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ClassPathXmlApplicationContext implements ApplicationContext{
/**
* 存储bean的Map集合
*/
private Map<String,Object> beanMap = new HashMap<>();
/**
* 在该构造方法中,解析myspring.xml文件,创建所有的Bean实例
* @param resource 配置文件路径(要求在类路径当中)
*/
public ClassPathXmlApplicationContext(String resource) {
try {
SAXReader reader = new SAXReader();
Document document = reader.read(ClassLoader.getSystemClassLoader().getResourceAsStream(resource));
// 获取所有的bean标签
List<Node> beanNodes = document.selectNodes("//bean");
// 第一次遍历:实例化所有Bean(不给属性赋值)
beanNodes.forEach(beanNode -> {
Element beanElt = (Element) beanNode;
String id = beanElt.attributeValue("id");
String className = beanElt.attributeValue("class");
try {
Class<?> clazz = Class.forName(className);
Constructor<?> defaultConstructor = clazz.getDeclaredConstructor();
Object bean = defaultConstructor.newInstance();
beanMap.put(id, bean);
} catch (Exception e) {
e.printStackTrace();
}
});
// 第二次遍历:给Bean的所有属性赋值
// 这样做是为了解决循环依赖问题:实例化和属性赋值分开
beanNodes.forEach(beanNode -> {
Element beanElt = (Element) beanNode;
String beanId = beanElt.attributeValue("id");
List<Element> propertyElts = beanElt.elements("property");
propertyElts.forEach(propertyElt -> {
try {
String propertyName = propertyElt.attributeValue("name");
Class<?> propertyType = beanMap.get(beanId).getClass().getDeclaredField(propertyName).getType();
String setMethodName = "set" + propertyName.toUpperCase().charAt(0) + propertyName.substring(1);
Method setMethod = beanMap.get(beanId).getClass().getDeclaredMethod(setMethodName, propertyType);
String propertyValue = propertyElt.attributeValue("value");
String propertyRef = propertyElt.attributeValue("ref");
Object propertyVal = null;
if (propertyValue != null) {
// 简单属性赋值
String propertyTypeSimpleName = propertyType.getSimpleName();
switch (propertyTypeSimpleName) {
case "byte": case "Byte":
propertyVal = Byte.valueOf(propertyValue);
break;
case "short": case "Short":
propertyVal = Short.valueOf(propertyValue);
break;
case "int": case "Integer":
propertyVal = Integer.valueOf(propertyValue);
break;
case "long": case "Long":
propertyVal = Long.valueOf(propertyValue);
break;
case "float": case "Float":
propertyVal = Float.valueOf(propertyValue);
break;
case "double": case "Double":
propertyVal = Double.valueOf(propertyValue);
break;
case "boolean": case "Boolean":
propertyVal = Boolean.valueOf(propertyValue);
break;
case "char": case "Character":
propertyVal = propertyValue.charAt(0);
break;
case "String":
propertyVal = propertyValue;
break;
}
setMethod.invoke(beanMap.get(beanId), propertyVal);
}
if (propertyRef != null) {
// 非简单属性赋值
setMethod.invoke(beanMap.get(beanId), beanMap.get(propertyRef));
}
} catch (Exception e) {
e.printStackTrace();
}
});
});
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Object getBean(String beanId) {
return beanMap.get(beanId);
}
}
第四步:测试自定义框架
配置文件 myspring.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="userServiceBean" class="com.powernode.myspring.bean.UserService">
<property name="userDao" ref="userDaoBean"/>
</bean>
<bean id="userDaoBean" class="com.powernode.myspring.bean.UserDao"/>
</beans>
测试代码:
@Test
public void testMySpring(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("myspring.xml");
UserService userServiceBean = (UserService) applicationContext.getBean("userServiceBean");
userServiceBean.save();
}
Spring IoC注解式开发
注解开发优势
Spring6倡导全注解开发,注解的存在主要是为了简化XML的配置。
注解基础回顾
1. 注解定义
@Target(value = {ElementType.TYPE})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Component {
String value();
}
2. 注解使用
@Component(value = "userBean")
// 或者简写为
@Component("userBean")
public class User {
}
3. 反射读取注解
通过反射读取注解示例
public class Test {
public static void main(String[] args) throws Exception {
Map<String,Object> beanMap = new HashMap<>();
String packageName = "com.powernode.bean";
String path = packageName.replaceAll("\\.", "/");
URL url = ClassLoader.getSystemClassLoader().getResource(path);
File file = new File(url.getPath());
File[] files = file.listFiles();
Arrays.stream(files).forEach(f -> {
String className = packageName + "." + f.getName().split("\\.")[0];
try {
Class<?> clazz = Class.forName(className);
if (clazz.isAnnotationPresent(Component.class)) {
Component component = clazz.getAnnotation(Component.class);
String beanId = component.value();
Object bean = clazz.newInstance();
beanMap.put(beanId, bean);
}
} catch (Exception e) {
e.printStackTrace();
}
});
System.out.println(beanMap);
}
}
声明Bean的注解
Spring提供了四个用于声明Bean的注解:
注解 | 说明 | 适用场景 |
---|---|---|
@Component | 通用组件 | 通用Bean |
@Controller | 控制器组件 | Web层控制器 |
@Service | 服务组件 | 业务逻辑层 |
@Repository | 数据访问组件 | 数据访问层 |
注解关系
@Controller
、@Service
、@Repository
都是@Component
的别名,功能完全相同,只是为了增强代码可读性。
使用注解开发的步骤
第一步:添加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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
</beans>
第二步:指定扫描包
<!-- 扫描单个包 -->
<context:component-scan base-package="com.powernode.spring6.bean"/>
<!-- 扫描多个包(逗号分隔) -->
<context:component-scan base-package="com.powernode.spring6.bean,com.powernode.spring6.bean2"/>
<!-- 扫描父包 -->
<context:component-scan base-package="com.powernode.spring6"/>
第三步:在Bean类上使用注解
@Component("userBean")
public class User {
// 类的内容
}
// 如果不指定value,默认bean名称为类名首字母小写
@Component
public class BankDao {
// bean名称为:bankDao
}
选择性实例化Bean
有时我们需要选择性地实例化某些类型的Bean:
包含过滤器(include-filter)
<!-- 只实例化Controller注解的Bean -->
<context:component-scan base-package="com.powernode.spring6.bean3"
use-default-filters="false">
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
排除过滤器(exclude-filter)
<!-- 排除Repository、Service、Controller注解的Bean -->
<context:component-scan base-package="com.powernode.spring6.bean3">
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Repository"/>
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Service"/>
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
负责注入的注解
Spring提供了多个用于依赖注入的注解:
注解 | 用途 | 适用类型 |
---|---|---|
@Value | 注入简单类型值 | 基本数据类型、String等 |
@Autowired | 自动装配 | 非简单类型 |
@Qualifier | 指定Bean名称 | 配合@Autowired使用 |
@Resource | 资源注入 | 非简单类型 |
@Value注解
@Value
注解用于注入简单类型的值:
@Component
public class User {
@Value("zhangsan")
private String name;
@Value("20")
private int age;
// 也可以用在setter方法上
@Value("李四")
public void setName(String name) {
this.name = name;
}
// 也可以用在构造方法参数上
public User(@Value("隔壁老王") String name, @Value("33") int age) {
this.name = name;
this.age = age;
}
}
@Autowired注解
@Autowired
注解用于自动装配非简单类型:
// DAO接口
public interface UserDao {
void insert();
}
// DAO实现类
@Repository
public class UserDaoForMySQL implements UserDao{
@Override
public void insert() {
System.out.println("正在向mysql数据库插入User数据");
}
}
// Service类
@Service
public class UserService {
@Autowired // 默认按类型装配
private UserDao userDao;
public void save(){
userDao.insert();
}
}
注意事项
@Autowired
默认按类型(byType)装配- 可以标注在:构造方法、方法、形参、属性、注解上
required
属性默认为true
,表示必须注入成功,否则报错
实际应用场景
1. 三层架构注解配置
// Controller层
@Controller
public class UserController {
@Autowired
private UserService userService;
public void handleRequest() {
userService.save();
}
}
// Service层
@Service
public class UserService {
@Autowired
private UserDao userDao;
public void save() {
userDao.insert();
}
}
// DAO层
@Repository
public class UserDaoImpl implements UserDao {
@Override
public void insert() {
System.out.println("插入用户数据");
}
}
2. 配置属性注入
@Component
public class DatabaseConfig {
@Value("${db.url}")
private String url;
@Value("${db.username}")
private String username;
@Value("${db.password}")
private String password;
}
最佳实践
1. 注解选择原则
- 控制器类:使用
@Controller
- 业务逻辑类:使用
@Service
- 数据访问类:使用
@Repository
- 通用组件类:使用
@Component
2. 包扫描配置
<!-- 推荐:按模块分包扫描 -->
<context:component-scan base-package="com.company.project.controller"/>
<context:component-scan base-package="com.company.project.service"/>
<context:component-scan base-package="com.company.project.dao"/>
3. 注入方式选择
- 简单类型:使用
@Value
- 对象类型:使用
@Autowired
- 需要指定Bean名称:使用
@Autowired
+@Qualifier
4. 代码简化
@Component
@Data // Lombok注解,自动生成getter/setter
public class User {
@Value("${user.name}")
private String name;
@Value("${user.age}")
private Integer age;
}
总结
本章我们学习了Spring注解式开发的核心内容:
- 反射机制:Spring框架实现的基础技术
- 手写Spring框架:理解IoC容器的实现原理
- 声明Bean注解:
@Component
、@Controller
、@Service
、@Repository
- 依赖注入注解:
@Value
、@Autowired
等 - 包扫描配置:选择性实例化Bean的方法
注解式开发大大简化了Spring的配置,提高了开发效率。在实际项目中,建议采用注解和XML配置相结合的方式,充分发挥各自的优势。
下一章预告
下一章我们将学习更多的依赖注入注解,包括@Qualifier
、@Resource
等,以及全注解开发的完整方案。