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进行依赖注入:
举例来说,如果您有一个数据访问层的接口,直接引用具体实现类可能如下所示:
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框架在企业级应用程序开发中如此受欢迎的一个重要原因。
既然已经知道了它主要的功能了,那么创建一个简单的 Spring 框架也就可以简单实现了,现在就选择里面最具有代表性的功能:基本的 IoC(控制反转)和 AOP(面向切面编程)功能,是一个有趣但复杂的任务。
首先,我们实现一个非常简单的 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 实例。
接下来,我们实现一个基础的 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
逻辑。
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
方法执行前后添加了一些额外的逻辑。
首先用产品思想,不需要知道如何实现,而是要问自己,我熟悉的spring可以做什么?
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 容器的扫描类的代码
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()。
接下来我们继续完成…未完成部分, 关注待续: