Spring教程
Spring教程#
前言
一、spring是什么
Spring是一个轻量级的控制反转(IOC)、面向切面编程(AOP)的容器。
- 特性:控制反转,面向切面编程;
- 支持事务处理、整合框架。
spring主要是作为springboot的基础学习的,spring中的配置文件还是很多,有些地方设置不完全。其中Bean的自动装配比较重要,对后面的学习意义重大,需要理解清楚。
另外java的反射内容需要多加理解
二、IOC
1.传统web开发#
在传统的web开发过程中,实现一个业务需要一下几个部分
- XxxDao:接口
- XxxDaoImpl:接口实现类
- XxxService:业务接口
- XxxServiceImpl:业务实现类
1.UserDao
public interface UserDao {
public void getinfo();
}
2.UserDaoImpl
public class UserDaoImpl implements UserDao{
@Override
public void getinfo() {
System.out.println("user");
}
}
3.UserService
public interface UserService {
public void getinfo();
}
4.UserServiceImpl
public class UserServiceImpl implements UserService {
//Service层:调用Dao层
private UserDao userDao = new UserDaoImpl();
@Override
public void getinfo() {
userDao.getinfo();
}
}
5.Test
@Test
public void test() {
//用户直接访问Service层,Service层调用Dao层
UserService userService = new UserServiceImpl();
userService.getUserInfo();
}
如果现在需要增添新用户,那么就需要在UserServiceImpl里改变相应的代码
//private UserDao userDao = new UserDaoImpl_01();
private UserDao userDao = new UserDaoImpl_02();
这样就出现了一系列问题:代码强耦合,复用性低,维护成本高。即违反了非必须不修改原代码的原则。那么我们让userDao根据set函数实现动态注入即可解决这个问题,即让程序不再有主动性,而是变成了被动的接受对象。
UserServiceImpl:
public class UserServiceImpl implements UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void getinfo() {
userDao.getinfo();
}
}
2.IOC本质#
- 控制反转IOC(Inversion of Control),是一种设计思想,DI(依赖注入)是实现IOC的一种方法
- 控制反转是一种通过描述(XML或注解)并通过第三方去生产或获取特定对象的方式。
- 程序员不再需要管理对象的创建,而是由用户自行根据需求选择。
3.IOC容器#
在Spring中实现控制反转的是IOC容器,其工作流程有三步:
- Spring 容器在初始化时先读取配置文件;
- Spring 容器根据元数据创建并组装Bean对象;
- 配置好的系统程序,使用时从容器中获取需要的 Bean 对象;
三、Bean
1.概念#
- Bean是被实例的,组装的及被Spring 容器管理的Java对象
- Spring容器创建、组装并管理Bean对象;
- 原理:通过反射实现,默认先通过无参构造创建实例,再通过getter()获取属性名,通过setter()设置属性值。
2.Bean的配置#
<!--
id:bean的唯一标识符,也就是相当于我们学的对象名
class:bean 对象所对应的权限定名:包名 + 类型
name: 也是别名,而且name更高级,可以起多个别名,通过逗号空格分号等分割
-->
<bean id="userT" class="pojo.User" name="user01,user02">
<property name="name" value="username"/>
</bean>
3.依赖注入#
依赖注入(Dependency Injection,DI):
- 依赖 : 指Bean对象的创建依赖于容器 . Bean对象的依赖资源 .
- 注入 : 指Bean对象所依赖的资源 , 由容器来设置和装配 .
- 构造器注入:
<!--有参构造函数注入 --> <!--根据索引注入--> <bean id="user" class="pojo.User"> <constructor-arg index="0" value="spring01"/> </bean> <!--根据参数名注入--> <bean id="user" class="pojo.User"> <constructor-arg name="name" value="spring02"/> </bean>
- setter方法注入:
<!-- 基本数据类型:使用value作为属性值; 引用数据类型:使用ref作为属性值,引用容器中的其他Bean --> <!-- setter注入 重点--> <bean id="person" class="pojo.Person"> <!--基本数据类型--> <property name="name" value="基本数据类型"/> <!--引用数据类型--> <property name="hello" ref="hello"/> <!--数组--> <property name="strings"> <array> <value>s1</value> <value>s2</value> </array> </property> <!--list--> <property name="list"> <list> <value>"l1"</value> <value>l2</value> </list> </property> <!--map--> <property name="map"> <map> <entry key="m1_key" value="m1_va"/> <entry key="m2_key" value="m2_va"/> </map> </property> <!--set--> <property name="set"> <set> <value>s1</value> <value>"s2"</value> </set> </property> <!--properties 格式:key=value --> <property name="properties"> <props> <prop key="p1_key">p1_value</prop> <prop key="p2_key">p2_value</prop> </props> </property> <!--null--> <property name="nulls"> <null/> </property> </bean>
4.获取Bean#
public class MyTest {
public static void main(String[] args) {
// 实例化容器,获取Spring的上下文对象
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//我们的对象现在都在Spring中管理了,如果要使用,直接去里面取出来就可以
//Hello hello = (Hello)context.getBean("hello");
Hello hello = context.getBean("hello",Hello.class);
System.out.println(hello);
}
}
5.Bean作用域#
Spring 默认机制是省略了<bean id=“user” class=“pojo.User” scope="singleton" />,也就是我们常说也是常用的单例模式。其余几个是在Web开发中用到的
四、Bean自动装配
Bean自动装配的方式有三种
- XML 显式配置:使用配置文件;
- 隐式自动装配(注解):自动注入属性值
- Java 显式配置:实现零配置文件;
1.XMl显示配置#
即上面一直在用的配置
使用 Bean 元素的 autowire 属性开启自动装配
<bean id="person" class="pojo.Person" autowire="***"/>
2.隐式自动装配(注解)#
- 注解有2种类型:类级别(Component、Scope)、类内部(Autowired、Value);
- IOC容器会自动注册类级别注解的 Bean,前提是有context:component-scan配置支持;
- 要使用类内部的注解,需要有相应处理器的支持,处理器需要在 XML 中注册;
通过context:annotation-config/标签来隐式注册处理器; - 要让类级别注解生效,就要context:component-scan配置支持,而这个标签又隐式启动了context:annotation-config/;
- 在使用时,我们只需要引入相关配置,以及context:component-scan标签。
@Component//等价于<bean id="user" class="pojo.User"/>
/*
@Component有几个衍生的注解。按照mvc三层架构分层,这几个注解分别对应到相应的层
dao 【 @Repository 】
service 【 @Service 】
controller 【 @Controller 】
这四个注解功能都是一样的,都是代表将某个注册类注入到Spring中,装配Bean
*/
@Scope("singleton")//等价于<bean id="user" class="pojo.User" scope="singleton"/>
public class Person {
@Autowired //引用数据类型
@Qualifier(value="cat")
private Cat cat;
@Autowired
/* 注解自动装配
@Autowired
工作机制:先 byType ,再 byName
1.如果容器中只存在一个匹配的 Bean ,则自动装配
2.如果容器中存在多个匹配的 Bean,则匹配属性名:
a.存在与属性同名的 Bean,自动装配;
b.不存在同名 Bean,可以添加注解 @Qualifier(value = "beanId") 来匹配 Bean
*/
private Dog dog;
@Value("username") //基本数据类型
//等价于<property name="name" value="**"/>
private String name;
@Bean(value = "getUser")//默认bean名为方法名,value可以设置bean名
public User getUser(){
return new User();
}
}
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!--导入 context 配置-->
<!-- 注解开发 扫描包下所有的类-->
<context:component-scan base-package="pojo"/>
<!-- 注解支持 -->
<context:annotation-config/>
</beans>
3.Java 显式配置#
上面的两种方法还要基于配置文件(beans.xml)来做
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Person person = context.getBean("person", Person.class);
接下来,我们可以完全不使用xml配置,全权交给java来做(这在springboot中是常见的),但是还是要利用注解
@Configuration//取代原本的 XML 配置
//被Spring容器托管,注册到容器里,因为他本来就是一个@Component(继承)
@ComponentScan(basePackages = "pojo")//相当于 XML 文件中的<context:component-scan/>标签
public class MyConfig {
@Bean("user")
public User getUser(){
return new User();
}
}
public static void main(String[] args) {
//如果完全使用了配置类,我们就只能通过AnnotationConfig 上下文来获取容器,通过配置类的class对象加载
ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
User getUser = (User)context.getBean("user");
}
五、代理模式
访问对象不适合或者不能直接引用目标对象,此时给目标对象提供一个代理以控制对该对象的访问,代理模式可以起到中介和保护作用,扩展和增强目标对象的功能。
- 静态:程序运行前。程序员在创建目标对象时创建代理类;
- 动态:程序运行时。运用反射机制动态创建。
1.静态代理#
结构 | 说明 | 举例 |
---|---|---|
抽象主题类(Subject) | 定义一个接口或抽象类,声明要实现的业务方法 | 租房业务 |
真实主题类(Real Subject) | 实现了抽象主题中的具体方法,是客户端最终实际访问的对象 | 房东 |
代理类(Proxy) | 实现了抽象主题类,提供一个接口来引用真实主题。 可以访问、控制和扩展真实主题的功能 | 房屋中介 |
客户端(Client) | 通过代理,访问真实主题的业务方法 | 找房子的人 |
抽象主题类(接口类):
public interface UserService {
public void show();
}
真实主题类(接口实现类):
public class UserServiceImpl implements UserService {
@Override
public void show() {
System.out.println("实现被代理对象的操作");
}
}
代理类:
public class UserServiceProxy implements UserService {
//静态代理
private UserServiceImpl userService;
public void setUserService(UserServiceImpl userService) {
this.userService = userService;
}
@Override
public void show() {
this.doit();
userService.show();
}
//实现面相切面编程
public void doit(){
System.out.println("实现接口操作的预操作/后续操作");
}
}
客户端:
UserServiceImpl userService = new UserServiceImpl();
UserServiceProxy userServiceProxy = new UserServiceProxy();
userServiceProxy.setUserService(userService);
userServiceProxy.show();
2.动态代理#
代理类:
//动态代理 代理的是接口实现类(不是接口类)
public class DynamicProxyHandler implements InvocationHandler {
//被代理对象
private Object object;
//设置被代理对象
public void setObject(Object object) {
this.object = object;
}
//获取代理对象 根据传入的参数,通过反射生成.class
//每一个代理对象都有一个关联的调用处理程序,当在代理对象上调用方法时,方法调用将被编码并分配到其调用处理程序的invoke方法
public Object getProxy() {
return Proxy.newProxyInstance(object.getClass().getClassLoader(),
object.getClass().getInterfaces(),
this);
}
//proxy 代理 method 要运行的方法 args 参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
pre();
// 执行被代理对象的方法
Object result = method.invoke(object, args);
post();
return result;
}
private void pre() {
System.out.println("代理:预处理");
}
private void post() {
System.out.println("代理:后续处理");
}
}
客户端:
public class mytest {
@Test
public void fn() {
//动态代理
//获取被代理对象
UserService userService = new UserServiceImpl();
//获取处理器
DynamicProxyHandler handler = new DynamicProxyHandler();
//设置被代理对象
handler.setObject(userService);
//获取代理对象
UserService proxy = (UserService)handler.getProxy();
//通过代理对象访问代理对象
proxy.show();
}
}
六、AOP
底层实现:代理模式
1.概念#
- AOP 是对 OOP 的补充(Object-oriented Programming,面向对象编程),并且允许自定义切面;
- OOP 中模块化的单位是类(Class),AOP 中模块化的单位是切面(Aspect);
- 切面能够实现跨类型和跨对象的横切关注点(如事务管理)的模块化;
- Spring IOC 容器不依赖于AOP,但 AOP 对 Spring IOC 进行了补充,提供了一个强大的中间件解决方案。
2.术语#
- 横切关注点:跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 , 安全 , 缓存 , 事务等等 …
- 切面(ASPECT):横切关注点 被模块化 的特殊对象。即,它是一个类。Log
- 通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。Log方法
- 目标(Target):被通知对象。接口
- 代理(Proxy):向目标对象应用通知之后创建的对象。代理类
- 切入点(PointCut):切面通知 执行的 “地点”的定义。method
- 连接点(JointPoint):与切入点匹配的执行点。invoke
3.实现#
需要导入依赖和spring配置
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
<?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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
方法一:使用Spring的API接口
-
业务接口和实现类:
public interface UserService { public void add(); } public class UserServiceImpl implements UserService{ @Override public void add() { System.out.println(1); } }
-
增强类:
public class Log implements MethodBeforeAdvice { @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println(target.getClass().getName()+method.getName()+" "+this.getClass().getName()); } }
-
实现aop切入实现
<bean id="log" class="log.Log"/> <!--方式1:原生spring API接口--> <!--配置aop : 需要导入aop的约束--> <aop:config> <!--切入点 expression:表达式匹配要执行的方法--> <aop:pointcut id="pointcut" expression="execution(* service.UserServiceImpl.*(..))"/> <!--执行环绕; advice-ref执行方法 . pointcut-ref切入点--> <aop:advisor advice-ref="log" pointcut-ref="pointcut"/> </aop:config>
方法2:自定义切面
- 自定义切入类:
public class pointcut { public void before(){ System.out.println("before"); } public void after(){ System.out.println("after"); } }
- spring配置:
<!--方式2:自定义--> <bean id="diy" class="log_div.pointcut"/> <aop:config> <!--自定义切面,ref:要引用的类--> <aop:aspect ref="diy"> <!--切入点--> <aop:pointcut id="point" expression="execution(* log_div.pointcut.*(..))"/> <!-- 通知 --> <aop:before method="before" pointcut-ref="point"/> <aop:after method="after" pointcut-ref="point"/> </aop:aspect> </aop:config>
方法3:注解实现
- 注解实现的增强类
@Aspect //标注这个类是个切面 public class annotationPointcut { @Before("execution(* service.UserServiceImpl.*(..))") public void before() { System.out.println("before"); } @After("execution(* service.UserServiceImpl.*(..))") public void after() { System.out.println("after"); } @Around("execution(* service.UserServiceImpl.*(..))") public void around(ProceedingJoinPoint joinPoint) { System.out.println("around before"); //连接点签名(方法) Signature signature = joinPoint.getSignature(); System.out.println(signature); //运行连接点方法(目标对象本身的方法) try { joinPoint.proceed(); } catch (Throwable e) { e.printStackTrace(); } System.out.println("around after"); } }
- spring配置
<!--方式3:注解实现--> <bean id="annotationPointcut" class="log_div.annotationPointcut"/> <!--开启注解支持--> <aop:aspectj-autoproxy/>
七、整合Mybatis
pom.xml
<dependencies>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<!-- 数据库连接 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<!-- MyBatis整合Spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
<!-- AOP 织入器 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
<!--spring-webmvc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.9</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
1. 方式一(SqlSessionTemplate)#
mybatis-spring.xml
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://localhost:3306?useSSL=false&useUnicode=true&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="20010823GTH"/>
</bean>
<!-- sqlSessionFactory
1、可以实现mybatis-config中所有的配置,取代mybatis-config的工作;
2、也可以绑定 mybatis-config ,二者同时存在
-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 绑定MyBatis配置文件 -->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!-- 注册Mapper 也可以放到mybatis-config.xml里面-->
<property name="mapperLocations" value="classpath:dao/UserMapper.xml"/>
</bean>
<!--SqlSessionTemplate
类似MyBatis中的SqlSession,
-->
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<!--没有setter,只能通过构造器注入(只有一个参数)-->
<constructor-arg ref="sqlSessionFactory"/>
</bean>
</beans>
UserMapperImpl
public class UserMapperImpl implements UserMapper{
//SqlSessionTemplate:获取Mapper接口,执行方法
private SqlSessionTemplate sqlSession;
public void setSqlSession(SqlSessionTemplate sqlSession) {
this.sqlSession = sqlSession;
}
@Override
public List<User> getUserList() {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
return mapper.getUserList();
}
}
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--合并其他配置文件,作为Spring 总配置-->
<!--导入的配置文件专门用于处理某项功能,分工明确-->
<!--1.有关 MyBatis 的配置都在mybatis-spring.xml中完成-->
<!--2.在 applicationContext 中注册 Bean-->
<import resource="mybatis-spring.xml"/>
<!-- Mapper接口实现类 -->
<!--方式1-->
<bean id="userMapper" class="dao.UserMapperImpl">
<property name="sqlSession" ref="sqlSessionTemplate"/>
</bean>
</beans>
2.方式二(SqlSessionDaoSupport)#
mybatis-spring.xml
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://localhost:3306?useSSL=false&useUnicode=true&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="20010823GTH"/>
</bean>
<!-- sqlSessionFactory
1、可以实现mybatis-config中所有的配置,取代mybatis-config的工作;
2、也可以绑定 mybatis-config ,二者同时存在
-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 绑定MyBatis配置文件 -->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!-- 注册Mapper 也可以放到mybatis-config.xml里面-->
<property name="mapperLocations" value="classpath:dao/UserMapper.xml"/>
</bean>
</beans>
UserMapperImpl
public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper{
@Override
public List<User> getUserList() {
return getSqlSession().getMapper(UserMapper.class).getUserList();
}
}
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--合并其他配置文件,作为Spring 总配置-->
<!--导入的配置文件专门用于处理某项功能,分工明确-->
<!--1.有关 MyBatis 的配置都在mybatis-spring.xml中完成-->
<!--2.在 applicationContext 中注册 Bean-->
<import resource="mybatis-spring.xml"/>
<!-- Mapper接口实现类 -->
<!--方式2-->
<bean id="userMapper02" class="dao.UserMapperImpl02">
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>
</beans>
八、事务管理
- 编程式事务:需要在代码中,进行事务的管理
- 将事务管理代码嵌到业务方法中来控制事务的提交和回滚
- 缺点:必须在每个事务操作业务逻辑中包含额外的事务管理代码 - 声明式事务:AOP
- 一般情况下比编程式事务好用。
- 将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
- 将事务管理作为横切关注点,通过aop方法模块化。
总结
作者:23DAY
出处:https://www.cnblogs.com/23DAY/p/16994505.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现