Spring Aop

文章目录

一、DI 注解之 Autowired 和 Value 注解(掌握)

1、DI 注解

因为之前 XML 配置方式比较繁琐。

xml 复制代码
<bean id="person" class="cn.wolfcode._01_hello.Person">
    <property name="name" value="大罗"/>
</bean>

分两类,Spring 的 Autowire,JavaEE 的 Resource,两者作用是一样,完成属性或字段的注入,注入是 bean(取代 XML property ref 元素)。而 Spring 的 Value 注解也是完成属性或字段的注入,注入是常量值(取代 XML property value 元素)。

2、Autowired 注解使用

2.1、编写类

java 复制代码
package cn.wolfcode._04_anno;

public class Dog {
	@Value("黄色")
    private String color;
	
	@Override
	public String toString() {
		return "Dog [color=" + color + "]";
	}
}
java 复制代码
package cn.wolfcode._04_anno;

public class Person {
	@Autowired
	private Dog dog;

	@Override
	public String toString() {
		return "Person [dog=" + dog + "]";
	}
}

2.2、编写配置文件

在 resources 目录下新建 04.anno.xml,配置如下:

xml 复制代码
<bean id="dog" class="cn.wolfcode._04_anno.Dog"/>

<bean id="person" class="cn.wolfcode._04_anno.Person"/>

<!-- 配置 DI 注解解析器,但其实可以不配置 -->
<context:annotation-config/>

2.3、编写单元测试类

java 复制代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:04.anno.xml")
public class PersonTest {
	@Autowired
	private Person person;

	@Test
	public void test() {
		System.out.println(person);
	}
}

3、Autowired 注解细节

  • 可以让 Spring 自动的把属性或字段需要的对象找出来,并注入到属性上或字段上。
  • 可以贴在字段或者 setter 方法上面。
  • 可以同时注入多个对象。
  • 可以注入一些 Spring 内置的重要对象,比如 BeanFactory,ApplicationContext 等。
  • 默认情况下 Autowired 注解必须要能找到对应的对象,否则报错。通过 required=false 来避免这个问题:@Autowired(required=false)。
  • 第三方程序:Spring3.0 之前需要手动配置 Autowired 注解的解析程序:<context:annotation-config/>,在 Spring 的测试环境可以不配置,在 Web 开发中换一种配置解决。
  • Autowired 注解寻找 bean 的方式:
    • 首先按照依赖对象的类型找,若找到,就是用 setter 或者字段直接注入。
    • 如果在 Spring 上下文中找到多个匹配的类型,再按照名字去找,若没有匹配报错。
    • 可以通过使用 @Qualifier("other") 标签来规定依赖对象按照 bean 的 id 和 类型的组合方式去找。

二、DI 注解之 Resource 注解(了解)

1、添加依赖

xml 复制代码
<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
</dependency>

2、Resource 注解使用

修改上面代码的 Person 类使用 Resource 注解,再重新测试即可。

java 复制代码
package cn.wolfcode._04_anno;

public class Person {
	@Resource
	private Dog dog;

	@Override
	public String toString() {
		return "Person [dog=" + dog + "]";
	}
}

3、Resource 注解细节

  • 可以让 Spring 自动的把属性或字段需要的对象找出来,并注入到属性上或字段上。
  • 可以贴在字段或者 setter 方法上面。
  • 可以注入一些 Spring 内置的重要对象,比如 BeanFactory,ApplicationContext 等。
  • Resource 注解必须要能找到对应的对象,否则报错。
  • 第三方程序:Spring3.0 之前需要手动配置 Autowired 注解的解析程序:<context:annotation-config/>,在 Spring 的测试环境可以不配置, Web 开发中换一种配置解决。
  • Resource 注解寻找 bean 的方式:
    • 首先按照名字去找,如果找到,就使用 setter 或者字段注入。
    • 若按照名字找不到,再按照类型去找,但如果找到多个匹配类型,报错。
    • 可以直接使用 name 属性指定 bean 的名称(@Resource(name=""));但若指定的 name,就只能按照 name 去找,若找不到,就不会再按照类型去。

4、Autowired 注解与 Resource 注解选用

都可以,出去后看公司。

三、IoC 注解(掌握)

1、XML 配置问题

Spring 的 XML 配置文件中还有很多 bean 的配置,那能不能让 Spring 通过什么方式来简化配置呢?

xml 复制代码
<bean id="dog1" class="cn.wolfcode._04_anno.Dog">
    <property name="color" value="黄色"/>
</bean>

<bean id="dog2" class="cn.wolfcode._04_anno.Dog">
    <property name="color" value="黑色"/>
</bean>

2、IoC 注解

四个注解的功能是相同的,只是用于标注不同类型的类上:

  • @Repository:用于标注数据访问组件,即 DAO 实现类上。
  • @Service:用于标注业务层实现类上。
  • @Controller:用于标注控制层类上(如 SpringMVC 的 Controller)。
  • @Component:当不是以上的话,可以使用这个注解进行标注。

3、IoC 注解使用

3.1、修改类

修改上面代码的 Person 类和 Dog 类,使用 Component 注解。

java 复制代码
package cn.wolfcode._04_anno;

@Component
public class Dog {
    @Value("黄色")
	private String color;

	@Override
	public String toString() {
		return "Dog [color=" + color + "]";
	}
}
java 复制代码
package cn.wolfcode._04_anno;

@Component
public class Person {
	@Autowired
	private Dog dog;

	@Override
	public String toString() {
		return "Person [dog=" + dog + "]";
	}
}

@Component 不写 value 值的话,bean 名字就类名首字母小写,上面列子就是 bean 的名字就是 person。

3.2、修改配置文件

在 04.anno.xml,配置如下:

xml 复制代码
<!-- 配置注解的第三方程序  解析 IoC 注解 DI 注解,让这些注解起作用-->
<context:component-scan base-package="cn.wolfcode._04_anno"/>

改完配置再测试一下看效果。

四、Scope 和 PostConstruct 以及 PreDestroy 注解(了解)

1、问题

比如之前使用 XML 方式配置 bean 的作用域等能不能也使用注解的方式?

2、注解说明

  • @Scope:贴在类上,标明 bean 的作用域。
  • @PostConstruct:贴在方法上,标明 bean 创建完后调用此方法。
  • @PreDestroy:贴在方法上,标明容器销毁时调用此方法。

3、注解使用

3.1、编写类

java 复制代码
package cn.wolfcode._04_anno;

@Component
@Scope("prototype")
public class MyDataSource {
	public MyDataSource() {
		System.out.println("对象创建");
	}
	public void getConnection() {
		System.out.println("拿到连接");
	}
	@PostConstruct
	public void init() {
		System.out.println("初始化池子");
	}
    @PreDestroy
	public void close() {
		System.out.println("销毁池子");
	}
}

3.2、编写配置文件

配置文件内容不变,还是跟上面一样。

3.3、编写单元测试类

java 复制代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:04.anno.xml")
public class MyDataSourceTest {
	@Autowired
	private MyDataSource dataSource;
	
	@Test
	public void test() {
		dataSource.getConnection();
	}
}

五、练习-使用注解配置模拟学生注册(掌握)

1、拷贝之前 XML 配置的项目

修改 pom.xml 的项目名为 spring-anno-register,再重新导入。

2、修改 DAO 层代码

java 复制代码
package cn.wolfcode.dao.impl;

@Repository
public class StudentDAOImpl implements IStudentDAO {
    @Autowired
	private DataSource dataSource;

	@Override
	public void save(String username, String password) throws Exception {
		@Cleanup
		Connection connection = dataSource.getConnection();
		@Cleanup
		PreparedStatement ps = connection.prepareStatement("INSERT INTO student(username, password) VALUES(?, ?)");
		ps.setString(1, username);
		ps.setString(2, password);
		ps.executeUpdate();
	}
}

3、修改业务层代码

java 复制代码
package cn.wolfcode.service.impl;

@Service
public class StudentServiceImpl implements IStudentService {
	@Autowired
	private IStudentDAO studentDAO;

	@Override
	public void register(String username, String password) throws Exception {
		studentDAO.save(username, password);
	}
}

4、修改 applicationContext.xml

xml 复制代码
<!-- 引入 db.properties -->
<context:property-placeholder location="classpath:db.properties"/>

<!-- 配置 DataSource bean -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
    init-method="init" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<!-- 配置第三方解析程序,让 IoC DI 注解起作用 -->
<context:component-scan base-package="cn.wolfcode"/>

六、控制事务繁琐(理解)

1、问题

考虑一个应用场景:需要对系统中的某些业务方法做事务管理,拿简单的 save 操作举例。没有加上事务控制的代码如下:

java 复制代码
public class EmployeeServiceImpl implements IEmployeeService {
    public void save(Employee employee){
        // 保存业务操作
    }
}

修改源代码,加上事务控制之后:

java 复制代码
public class EmployeeServiceImpl implements IEmployeeService {
    public void save(Employee employee){
        // 打开资源
        // 开启事务
        try {
            // 保存业务操作
            // 提交事务
        }catch (Exception e){
            // 回滚事务
        }finally{
            // 释放资源
        }
    }
}

上述问题:在我们的业务层中每一个业务方法都得处理事务(繁琐的 try-catch),这样设计上存在两个很严重问题:

  • 代码结构重复:在开发中不要重复代码,重复就意味着维护成本增大;
  • 责任不分离:业务方法只需要关心如何完成该业务功能,不需要去关系事务管理、日志管理、权限管理等等。

2、租房案例

问题:此时若有人来整房东,派很多人来找房东假租房,这会导致房东一天到晚都忙且没收获。带来这个问题就是:重复,且责任不分离,其实房东最关系的就是签合同和收房租。

后面我们就借鉴上面生活的例子的思想来之前事务繁琐的问题。

七、代理模式(了解)

1、好处

客户端直接使用的都是代理对象,不知道真实对象是谁,此时代理对象可以在客户端和真实对象之间起到中介的作用。

  • 代理对象完全包含真实对象,客户端使用的都是代理对象的方法,和真实对象没有直接关系;
  • 代理模式的职责:把不是真实对象该做的事情从真实对象上撇开---职责分离。

2、分类

  • 静态代理:在程序运行前就已经存在代理类的字节码文件,代理对象和真实对象的关系在运行前就确定了。(即代理类及对象要我们自己创建)
  • 动态代理:代理类是在程序运行期间由 JVM 通过反射等机制动态的生成的,所以不存在代理类的字节码文件,动态生成字节码对象,代理对象和真实对象的关系是在程序运行时期才确定的。(即代理类及对象不要我们自己创建)

3、动态代理实现方式

  • 针对真实类有接口使用 JDK 动态代理;
  • 针对真实类没实现接口使用 CGLIB 或 Javassist 组件。

4、动态代理实现机制

由于 JVM 通过字节码的二进制信息加载类的,若我们在运行期系统中,遵循 Java 编译系统组织 .class 文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据加载转换成对应的类。如此,就完成了在代码中动态创建一个类的能力了。

八、静态代理(了解)

1、需求

给业务类的方法增加模拟的事务。

2、类体系图

3、代码实现

新建一个 Maven 项目 static-proxy。

3.1、设置编译版本及添加依赖

xml 复制代码
<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>

3.2、编写业务接口及实现

java 复制代码
package cn.wolfcode.service;

public interface IEmployeeService {
	void save(String username, String password);
}
java 复制代码
package cn.wolfcode.service.impl;

// 房东
// 真实类,其对象为真实对象
public class EmployeeServiceImpl implements IEmployeeService {
	@Override
	public void save(String username, String password) {
		System.out.println("保存:" + username + ":" + password);
	}
}

3.3、编写代理类

java 复制代码
package cn.wolfcode.service.impl;

// 中介
// 代理类,其对象为代理对象
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();
		}
	}
}

3.4、编写事务模拟类

java 复制代码
package cn.wolfcode.tx;

public class MyTransactionManager {
	public void begin() {
		System.out.println("开启事务");
	}
	public void commit() {
		System.out.println("提交事务");
	}
	public void rollback() {
		System.out.println("回滚事务");
	}
}

3.5、编写 applicationContext.xml

在 resources 目录新建 applicationContext.xml,配置如下:

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>

3.6、编写单元测试类

java 复制代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class EmployeeServiceTest { 
    @Autowired
    private IEmployeeService proxy;
    
    @Test
    public void testSave() {
        // proxy 为代理对象
        proxy.save("罗老师", "666");
    }
}

4、优缺点

4.1、优点

  • 业务类只需要关注业务逻辑本身,保证了业务类的重用性。
  • 把真实对象隐藏起来了,保护真实对象。

4.2、缺点

  • 代理对象的某个接口只服务于某一种类型的对象,也就是为每个真实类创建一个代理类,比如项目还有其他业务类呢。
  • 若需要代理的方法很多,则要为每一种方法都进行代理处理。
  • 若接口增加一个方法,除了所有实现类需要实现这个方法外,代理类也需要实现此方法。

九、JDK 动态代理(了解)

1、需求

给业务类的方法增加模拟的事务。

2、JDK 动态代理 API

2.1、java.lang.reflect.Proxy

Java 动态代理机制生成的所有动态代理类的父类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。主要方法:public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler hanlder)

  • 方法职责:为指定类加载器、一组接口及调用处理器生成动态代理类实例。
  • 参数:
    • loader :类加载器,一般传递真实对象的类加载器;
    • interfaces:代理类需要实现的接口;
    • handler:代理执行处理器,说人话就是生成代理对象帮你要做什么。
  • 返回:创建的代理对象。

2.2、java.lang.reflect.InvocationHandler

主要方法:public Object invoke(Object proxy, Method method, Object[] args)。

  • 方法职责:负责集中处理动态代理类上的所有方法调用,让使用者自定义做什么事情, 对原来方法增强(加什么功能)。
  • 参数:
    • proxy :生成的代理对象;
    • method:当前调用的真实方法对象;
    • args :当前调用方法的实参。
  • 返回:真实方法的返回结果。

3、代码实现

3.1、拷贝静态代理项目

  • 修改项目名为 jdk-dynamic-proxy,重新导入;
  • 删除自己手写的代理类,配置文件报错顺便解决。

3.2、编写 TransactionInvocationHandler 类

该实现 InvocationHandler 接口,实现 invoke 方法,实现增强操作。

java 复制代码
package cn.wolfcode.handler;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import cn.wolfcode.tx.MyTransactionManager;

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; // 返回方法调用的结果
	}
}

3.3、修改 applicationContext.xml

配置 TransactionInvocationHandler、MyTransactionManager、EmployeeServiceImpl,让 Spring 帮我们创建这些对象组装依赖。

xml 复制代码
<!-- 配置事务管理器对象 -->
<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>

3.4、修改单元测试类

注入类型 InvocationHandler 的 bean,在测试方法中手动使用 Proxy 中的方法创建代理对象,调用代理对象的方法。

java 复制代码
@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(),
            // 通过这个参数告诉 API 生成代理对象具体做什么
            transactionInvocationHandler);
        proxy.save("罗老师", "666");
    }

}

4、优缺点

4.1、优点

  • 对比静态代理,发现不需手动地提供那么多代理类。

4.2、缺点

  • 真实对象必需实现接口(JDK 动态代理特有);
  • 动态代理的最小单位是类(类中某些方法都会被处理),如果只想拦截一部分方法,可以在 invoke 方法中对要执行的方法名进行判断;
  • 对多个真实对象进行代理的话,若使用 Spring 的话配置太多了,要手动创建代理对象,用起来麻烦。

十、JDK 动态代理原理(了解)

1、获取代理类字节码文件

ProxyGenerator 在 JDK 11 已经不是公有的了。

在包路径 cn.wolfcode.service.impl 下新建一个类 DynamicProxyClassGenerator,拷贝如下代码:

java 复制代码
public class DynamicProxyClassGenerator {
	public static void main(String[] args) throws Exception {
		generateClassFile(EmployeeServiceImpl.class, "EmployeeServiceProxy");
	}
	public static void generateClassFile(Class<?> targetClass, String proxyName) throws Exception {
		// 根据类信息和提供的代理类名称,生成字节码
		byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, targetClass.getInterfaces());
		String path = targetClass.getResource(".").getPath();
		System.out.println(path);
		FileOutputStream out = null;
		// 保留到硬盘中
		out = new FileOutputStream(path + proxyName + ".class");
		out.write(classFile);
		out.close();
	}
}

2、通过反编译工具查看字节码文件

使用 IDEA 就可以反编译。观察:save 方法,发现底层其实依然在执行 InvocationHandler 中的 invoke 方法。

3、类体系及调用流程

十一、CGLIB 动态代理(了解)

1、JDK 动态代理的问题

JDK 动态代理要求真实类必须实现接口。而 CGLIB 与 JDK 动态代理不同是,真实类不用实现接口,生成代理类的代码不一样且代理类会继承真实类。

2、CGLIB 动态代理 API

2.1、org.springframework.cglib.proxy.Enhancer

类似 JDK 中 Proxy,用来生成代理类创建代理对象的。

2.2、org.springframework.cglib.proxy.InvocationHandler

类似 JDK 中 InvocationHandler,让使用者自定义做什么事情,对原来方法增强。

3、代码实现

3.1、拷贝 JDK 动态代理项目

修改项目名为 jdk-cglib-proxy,重新导入。

3.2、修改 TransactionInvocationHandler 类

修改其实现接口为 org.springframework.cglib.proxy.InvocationHandler,其他不变。

3.3、修改单元测试类

改用 Enhancer API 来生成代理类创建代理对象。

java 复制代码
@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");
	}

}

4、类体系及调用流程

十二、动态代理总结(了解)

1、JDK 动态代理总结

  • Java 动态代理是使用 java.lang.reflect 包中的 Proxy 类与 InvocationHandler 接口这两个来完成的。
  • 要使用 JDK 动态代理,真实类必须实现接口。
  • JDK 动态代理将会拦截所有 pubic 的方法(因为只能调用接口中定义的方法),这样即使在接口中增加了新的方法,不用修改代码也会被拦截。
  • 动态代理的最小单位是类(类中某些方法都会被处理),如果只想拦截一部分方法,可以在 invoke 方法中对要执行的方法名进行判断。
  • 代理类与真实类共同实现相同接口的

2、CGLIB 动态代理总结

  • CGLIB 可以生成真实类的子类,并重写父类非 final 修饰符的方法。
  • 要求类不能是 final 的,要拦截的方法要是非 final、非 static、非 private 的。
  • 动态代理的最小单位是类(类中某些方法都会被处理),如果只想拦截一部分方法,可以在 invoke 方法中对要执行的方法名进行判断。
  • 代理类是继承真实类

3、选用

JDK 动态代理是基于实现接口的,CGLIB 和 Javassit 是基于继承真实类的。

从性能上考虑:Javassit > CGLIB > JDK。

选用如下:

  • 若真实类实现了接口,优先选用 JDK 动态代理。(因为会产生更加松耦合的系统,也更符合面向接口编程)
  • 若真实类没有实现任何接口,使用 Javassit 和 CGLIB 动态代理。
相关推荐
NiNg_1_2342 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Chrikk3 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*3 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue3 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man3 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
Wx-bishekaifayuan5 小时前
django电商易购系统-计算机设计毕业源码61059
java·spring boot·spring·spring cloud·django·sqlite·guava
customer085 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
小白冲鸭5 小时前
【报错解决】使用@SpringJunitConfig时报空指针异常
spring·java后端开发