看着spring源码,写框架(第1天)
目录
- 学习spring的第一天
- 1. 关于spring主要解决什么问题,而我们又该怎么做。
- 2. 开始动手
- 1. IoC 容器的简单实现
- 2. AOP 的简单实现
- 3. 示例使用
- 3. 如何延伸
- 3.1 需求分析
- 3.2 小试牛刀,实现获取资源配置功能
1. 关于spring主要解决什么问题,而我们又该怎么做。
Spring框架的设计初衷是为了解决企业级应用程序开发中的一系列复杂性和困难。下面我将详细解释每个方面,并提供一个例子来说明。
-
轻量级容器:
Spring引入了一个轻量级的IoC(控制反转)容器,以管理应用程序中的组件。设计初衷是减少了传统的重量级J2EE容器的复杂性。如果没有Spring,开发人员需要手动管理对象的生命周期和依赖关系。例如,使用Spring的IoC容器,您可以这样定义一个bean:
<bean id="myService" class="com.example.MyService" />
这样,Spring会负责创建和管理
MyService
类的实例,您只需通过容器获取它,而不必自己实例化对象。 -
面向切面编程(AOP):
Spring引入了AOP的概念,允许在应用程序中轻松地管理交叉关注点的关注点。它解决了代码耦合和重复性问题。例如,您可以使用Spring的AOP功能来记录方法的执行时间,而不必在每个方法中手动添加时间记录代码。
-
声明性事务管理:
Spring提供了声明式事务管理的支持,使开发人员能够通过注释或XML配置来定义事务。这简化了事务处理,降低了错误发生的概率。例如,使用Spring的事务管理,您可以这样配置一个方法为一个事务:
@Transactional public void performTransaction() { // 事务处理代码 }
-
松耦合:
Spring鼓励使用接口和依赖注入来实现松耦合,使得组件之间的关系更加灵活。这有助于测试和维护代码。例如,您可以定义一个接口,然后使用Spring进行依赖注入,而不是直接引用具体的实现类。
-
简化JDBC和JMS:
Spring提供了用于数据库访问和消息传递的简化API,使开发人员可以更容易地执行这些任务。例如,Spring的JdbcTemplate使得在使用JDBC时编写更少的模板代码成为可能。
如果没有Spring,开发企业级应用程序可能会更加复杂,代码会更加冗长,开发过程中需要处理的低级细节会增多,从而增加了出错的可能性。Spring的设计初衷是通过提供一套强大的功能和易于使用的工具来简化这些任务,从而提高开发效率并降低错误的风险。
当直接引用具体实现类和使用Spring进行依赖注入时,代码的结构和行为有很大的不同。下面我将详细解释这两种方法的特点和区别。
直接引用具体实现类:
- 紧耦合:在这种方法中,您的代码将直接引用特定的实现类,这意味着您的组件与该实现类之间存在紧耦合。这会导致代码的可维护性较差,因为更改实现类或切换到不同的实现会涉及到修改依赖该实现的所有代码。
- 难于测试:由于紧耦合,编写单元测试变得更加困难。您可能需要为测试创建实际实现的替代品,这增加了测试的复杂性。
- 难以实现替代方案:如果需要在不同的环境中使用不同的实现,或者需要为同一接口的不同实现提供不同的配置,这将变得非常困难。您可能需要在代码中添加条件语句来处理这些情况,从而增加了复杂性。
使用Spring进行依赖注入:
- 松耦合:Spring的依赖注入机制允许您通过接口或抽象类定义依赖关系,而不是直接引用特定的实现类。这导致了松散的耦合,使得代码更加灵活和可维护。
- 易于测试:由于依赖关系由Spring容器管理,您可以轻松地使用模拟对象或桩来测试代码,而无需依赖于实际实现。这使得编写单元测试变得更加容易。
- 可配置性:使用Spring,您可以通过配置文件或注释来定义依赖关系,因此可以在不修改代码的情况下切换不同的实现或提供不同的配置。这增加了应用程序的可配置性和可扩展性。
举例来说,如果您有一个数据访问层的接口,直接引用具体实现类可能如下所示:
MyDataAccess dao = new MySqlDataAccess();
而使用Spring进行依赖注入则可以这样:
@Autowired
private MyDataAccess dao;
通过Spring的依赖注入,您可以轻松切换不同的数据访问实现,而不必修改代码,这对于维护和扩展应用程序非常有帮助。同时,测试也变得更加简便,因为您可以使用模拟或替代的数据访问实现来进行单元测试。
如果没有Spring依赖注入,您需要手动管理依赖关系,这可能需要一些修改和调整代码。以下是在没有Spring依赖注入的情况下,您可能需要进行的一些代码修改:
-
手动实例化依赖对象:您需要在需要依赖的类中手动创建依赖对象的实例。这通常涉及使用
new
关键字来实例化具体的实现类。MyDataAccess dao = new MySqlDataAccess(); // 手动实例化依赖对象
-
硬编码依赖关系:您需要硬编码依赖关系,将具体的实现类引用直接嵌入到代码中。这会导致紧耦合,如果需要更改依赖关系,必须修改源代码。
public class MyService { private MyDataAccess dao = new MySqlDataAccess(); // 硬编码依赖关系 // 其他方法 }
-
缺乏灵活性:在没有依赖注入的情况下,切换到不同的实现类或配置不同的依赖关系将需要直接修改代码,这可能会导致不便和风险。
// 切换到不同的实现类,需要修改代码 public class MyService { private MyDataAccess dao = new OracleDataAccess(); // 修改依赖关系 // 其他方法 }
-
难以进行单元测试:因为依赖关系被硬编码到类中,所以在单元测试中难以使用模拟对象或桩。您可能需要编写更复杂的测试用例。
// 单元测试难以模拟依赖对象 public class MyServiceTest { @Test public void testMyService() { // 难以模拟MyDataAccess对象 // 需要测试的代码 } }
总之,没有Spring依赖注入,您将需要手动管理依赖关系,这可能会导致紧耦合、难以维护和测试的代码。Spring的依赖注入机制使代码更加灵活、可维护和易于测试,因此通常被广泛采用以提高代码质量和开发效率。
Spring的依赖注入机制是通过容器来管理和注入依赖对象的,它可以显著简化应用程序的配置和开发过程。以下是Spring执行上述工作的方式:
-
配置依赖关系:在Spring中,您可以通过XML配置文件、Java注解或Java配置类来定义依赖关系。您可以指定接口或抽象类作为依赖,并告诉Spring应该注入哪个具体的实现类。
例如,通过XML配置文件:
<bean id="myService" class="com.example.MyService"> <property name="dataAccess" ref="mySqlDataAccess" /> </bean> <bean id="mySqlDataAccess" class="com.example.MySqlDataAccess" />
这里,我们配置了
MyService
需要注入一个名为mySqlDataAccess
的依赖对象。 -
Spring容器管理依赖关系:当应用程序启动时,Spring容器会读取配置文件并创建相应的对象,包括依赖对象。它会负责创建依赖对象的实例,并确保它们按需注入到其他对象中。
-
自动装配:Spring支持自动装配,这意味着您可以不必显式指定每个依赖关系,而是根据规则让Spring自动查找并注入依赖。这可以减少配置的工作量。
例如,使用自动装配的注解:
@Autowired private MyDataAccess dataAccess; // Spring会自动注入适当的实现类
-
松耦合:由于Spring负责管理依赖关系,您的代码不再需要硬编码依赖对象的实现类。这导致了松散的耦合,使得您可以轻松切换不同的实现或配置不同的依赖关系,而不必修改代码。
-
易于测试:由于依赖关系由Spring容器管理,您可以轻松使用模拟对象或桩来进行单元测试,而不必依赖于实际实现。这使得编写单元测试变得更加容易。
-
配置的灵活性:Spring允许您根据需要动态更改配置,而无需修改代码。您可以通过修改配置文件或更改注解来切换依赖对象。
总之,Spring的依赖注入机制通过容器管理依赖关系,使代码更加灵活、可维护和易于测试。它通过配置来指定依赖关系,减少了硬编码,使得应用程序更容易扩展和维护。这是Spring框架在企业级应用程序开发中如此受欢迎的一个重要原因。
2. 开始动手
既然已经知道了它主要的功能了,那么创建一个简单的 Spring 框架也就可以简单实现了,现在就选择里面最具有代表性的功能:基本的 IoC(控制反转)和 AOP(面向切面编程)功能,是一个有趣但复杂的任务。
1. IoC 容器的简单实现
首先,我们实现一个非常简单的 IoC 容器,它可以创建对象实例并管理它们的生命周期。
import java.util.HashMap;
import java.util.Map;
public class SimpleIoCContainer {
private Map<Class<?>, Object> beans = new HashMap<>();
public void addBean(Class<?> clazz) throws Exception {
beans.put(clazz, clazz.newInstance());
}
public <T> T getBean(Class<T> clazz) {
return clazz.cast(beans.get(clazz));
}
}
这个 IoC 容器使用一个 HashMap
来存储 bean 实例。addBean
方法创建一个新实例并将其存储在 map 中。getBean
方法用于检索 bean 实例。
2. AOP 的简单实现
接下来,我们实现一个基础的 AOP 支持,允许在方法执行前后添加逻辑。
public interface MethodInvocation {
void invoke();
}
public class SimpleAOP {
public static Object getProxy(Object target, MethodInvocation before, MethodInvocation after) {
return java.lang.reflect.Proxy.newProxyInstance(
SimpleAOP.class.getClassLoader(),
target.getClass().getInterfaces(),
(proxy, method, args) -> {
if (before != null) {
before.invoke();
}
Object result = method.invoke(target, args);
if (after != null) {
after.invoke();
}
return result;
});
}
}
在这个简单的 AOP 实现中,我们使用 Java 的动态代理。getProxy
方法创建一个代理,该代理在目标方法执行前后执行提供的 before
和 after
逻辑。
3. 示例使用
public interface GreetingService {
void greet();
}
public class GreetingServiceImpl implements GreetingService {
@Override
public void greet() {
System.out.println("Hello, World!");
}
}
public class Main {
public static void main(String[] args) throws Exception {
// IoC
SimpleIoCContainer container = new SimpleIoCContainer();
container.addBean(GreetingServiceImpl.class);
GreetingService greetingService = container.getBean(GreetingServiceImpl.class);
// AOP
GreetingService proxy = (GreetingService) SimpleAOP.getProxy(
greetingService,
() -> System.out.println("Before method"),
() -> System.out.println("After method")
);
proxy.greet();
}
}
在这个示例中,我们创建了一个简单的 IoC 容器,并向其中添加了 GreetingService
的实现。然后,我们使用我们的 AOP 工具来创建一个代理,该代理在 greet
方法执行前后添加了一些额外的逻辑。
3. 如何延伸
3.1 需求分析
首先用产品思想,不需要知道如何实现,而是要问自己,我熟悉的spring可以做什么?
- 可以扫描到业务的java代码bean(class的形式被扫描到)
- 依赖jar的bean(class的形式被扫描到)
- 获取资源配置
- 可以让用户配置属性到properties
- 可以拥有一个Map字典一样的容器
- 可以实现类似代理的工作,让代理工具进行一个日志等扩展
- IoC容器的完成
- 集成以上步骤
- 验证和测试
3.2 小试牛刀,实现获取资源配置功能
package com.crab.io;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ResourceResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(ResourceResolver.class);
private final String basePackage;
public ResourceResolver(String basePackage) {
this.basePackage = basePackage;
}
public <T> List<T> scanResources(Function<Resource, T> function) {
String basePackagePath = replaceDotWithSlash(this.basePackage);
try {
List<T> results = new ArrayList<>();
scanResources(basePackagePath, results, function);
return results;
} catch (IOException | URISyntaxException e) {
throw new RuntimeException(e);
}
}
private <T> void scanResources(String basePackagePath, List<T> collector, Function<Resource, T> function)
throws IOException, URISyntaxException {
LOGGER.debug("Scanning path: {}", basePackagePath);
Enumeration<URL> urls = getResourceUrls(basePackagePath);
while (urls.hasMoreElements()) {
evaluateUrl(urls.nextElement(), basePackagePath, collector, function);
}
}
private String replaceDotWithSlash(String s) {
return s.replace(".", "/");
}
private Enumeration<URL> getResourceUrls(String path) throws IOException {
return getClassLoader().getResources(path);
}
private ClassLoader getClassLoader() {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
if (loader == null) {
loader = getClass().getClassLoader();
}
return loader;
}
private <T> void evaluateUrl(URL url, String basePackagePath, List<T> collector, Function<Resource, T> function)
throws URISyntaxException, IOException {
URI uri = url.toURI();
String urlString = removeEndSlash(decodeUri(uri));
String uriBase = urlString.substring(0, urlString.length() - basePackagePath.length());
uriBase = uriBase.startsWith("file:") ? uriBase.substring(5) : uriBase;
if (urlString.startsWith("jar:")) {
scanResource(true, uriBase, uriToPath(basePackagePath, uri), collector, function);
} else {
scanResource(false, uriBase, Paths.get(uri), collector, function);
}
}
private String decodeUri(URI uri) {
return URLDecoder.decode(uri.toString(), StandardCharsets.UTF_8);
}
private String removeEndSlash(String s) {
return s.endsWith("/") ? s.substring(0, s.length() - 1) : s;
}
private Path uriToPath(String basePackagePath, URI jarUri) throws IOException {
return FileSystems.newFileSystem(jarUri, Map.of()).getPath(basePackagePath);
}
private <T> void scanResource(boolean isJar, String base, Path root, List<T> results, Function<Resource, T> function)
throws IOException {
Files.walk(root)
.filter(Files::isRegularFile)
.forEach(file -> applyFunctionOnResource(isJar, removeEndSlash(base), file, results, function));
}
private String removeSlashFromStart(String s) {
return s.startsWith("/") ? s.substring(1) : s;
}
private <T> void applyFunctionOnResource(boolean isJar, String base, Path file, List<T> results,
Function<Resource, T> function) {
Resource res = isJar ?
new Resource(base, removeSlashFromStart(file.toString())) :
new Resource("file:" + file, removeSlashFromStart(file.toString().substring(base.length())));
T result = function.apply(res);
if (result != null) {
results.add(result);
}
}
}
public ResourceResolver(String basePackage): 构造函数,初始化ResourceResolver实例的basePackage属性。
public
<R>
List<R>
scan(Function<Resource, R> mapper): 此方法是外部调用的主要入口点,它接收一个将Resource对象转换为任意类型R的映射函数作为参数。该方法首先将basePackage中的".“替换为”/",然后调用scan0方法以执行主要的扫描工作。如果在执行过程中抛出IOException或URISyntaxException,该方法会将它们重新抛出为运行时异常。
<R>
void scan0(String basePackagePath, String path, List<R>
collector, Function<Resource, R> mapper) throws IOException, URISyntaxException: 此方法执行实际的扫描操作。它扫描给定路径并使用映射函数将找到的资源转换为类型R的对象,这些对象然后被添加到collector列表中。ClassLoader getContextClassLoader(): 此方法返回当前线程的上下文类加载器,如
果没有设置,则返回此类的类加载器。Path jarUriToPath(String basePackagePath, URI jarUri) throws IOException: 此方法将给定的jarUri转换为Path对象。
<R>
void scanFile(boolean isJar, String base, Path root, List<R>
collector, Function<Resource, R> mapper) throws IOException: 此方法对给定的根路径执行文件扫描,并使用映射函数将找到的资源转换为类型R的对象,这些对象然后被添加到collector列表中。String uriToString(URI uri): 此方法将给定的uri转换为字符串。
String removeLeadingSlash(String s): 删除字符串s的头部斜线,如果存在的话。
String removeTrailingSlash(String s): 删除字符串s的尾部斜线,如果存在的话。
package com.crab.io;
public record Resource(String path, String name) {
}
一个资源类
上面就是spring 容器的扫描类的代码
接下来就是一个测试类的代码
- 分别是对于资源类文件的扫描类测试 resource文件下取名test.txt文件
- 对依赖的jar包的扫描类测试 导入maven,jar包。 测试时候填jar的包名
- 对普通的Java编译的class文件的扫描类测试 随便写Java的helloworld
package com.crab.io;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.List;
import java.util.stream.Stream;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.sql.DataSourceDefinition;
import jakarta.annotation.sub.AnnoScan;
/**
* 执行资源扫描的测试套件
*/
public class ResourceResolverTest {
/**
* 该测试验证在特定包中可以找到匹配某些文件扩展名的资源。用于运行不同的包名和资源文件的参数化测试
* @param 包要扫描的包
* @param extension 要定位资源的文件扩展名
*/
@ParameterizedTest
@MethodSource("resourceParams")
public void testScanResource(String pkg, String extension) {
var rr = new ResourceResolver(pkg);
List<String> resourcesList = rr.scan(res -> processResourceName(res, extension));
System.out.print(resourcesList);
}
/**
* 测试扫描过程中是否找到了某些类
*/
@Test
public void shouldContainScannedClasses() {
...
assertTrue(classes.contains(clazz));
...
}
// 其他的方法
/**
* 此方法为参数化测试提供数据。数据包括包名和资源文件扩展名
* @return 参数数组的流。
*/
private static Stream<Object[]> resourceParams() {
return Stream.of(
new Object[] {"com.crab.scan", ".class"},
new Object[] {PostConstruct.class.getPackageName(), ".class"},
new Object[] {"com.crab.scan", ".txt"}
);
}
/**
* 根据资源类型(`.class`或`.txt`)处理资源名称并应用相应策略
* @param res 要处理的资源
* @param extension 资源文件扩展名
* @return 处理过的资源名称
*/
private String processResourceName(Resource res, String extension) {
String name = res.name();
if (name.endsWith(extension)) {
if (".class".equals(extension)) {
return name.substring(0, name.length() - 6).replace("/", ".").replace("\\", ".");
}
if (".txt".equals(extension)) {
return name.replace("\\", "/");
}
}
return null;
}
}
该类中有三个测试方法,分别为 scanClass()、scanJar() 和 scanTxt()。
- 扫描类()
此测试方法扫描类资源并检查指定包 com.itranswarp.scan 中是否存在所需的类。如果资源名称以“.class”结尾,则将其理解为 Java 类文件,并通过删除“.class”扩展名、将目录分隔符转换为点(在 Java 包名称中使用)并将其添加到类名列表。
然后对返回的类名列表进行排序,并且测试用例检查该列表是否包含一组特定的类名。 - 扫描Jar()
此方法似乎检查 Jakarta 注释库中的某些类是否已被 ResourceResolver 成功扫描。 PostConstruct 类上调用的 getPackageName() 方法检索包名称,ResourceResolver 扫描此包中的类文件。测试断言返回的类列表中存在相关的 Jakarta 类名。 - 扫描文本()
此方法测试扫描 com.itranswarp.scan 包中的文本文件 (.txt)。它使用 .endsWith(“.txt”) 条件来过滤掉非文本文件的文件。找到 .txt 文件后,它将 \ 目录分隔符替换为 / 并将结果添加到列表中。然后将排序后的列表与预期的文本文件列表进行比较。
接下来我们继续完成…未完成部分, 关注待续:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!