SpringBoot学习笔记
1.1、简介
springBoot优点
- 为所有Spring开发者快速入门
- 开箱即用,提供各种默认配置来简化项目配置
- 内嵌式容器简化Web项目
- 没有冗余代码生成和XML配置的要求
微服务:https://www.cnblogs.com/liuning8023/p/4493156.html
个人理解:微服务,是将一个整体程序拆分成很多部分(服务),这些服务改动后,只需要局部更新,不用更新整体。单个服务编程语言、数据库不同,也可以互相连接。
生成springboot
-
在官网生成,生成后导入:https://start.spring.io
Dependencies要加上Spring Web
-
在idea生成:新建项目>Spring Initializr即可
分析
<!--web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--单元测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--打jar包插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
所以spring依赖都是通过spring-boot-starter这个开头的
打jar包,点maven的Lifecycle下的package
运行:java -jar *.jar
1.2、springBoot自动装配原理
启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
- 启动器是springboot的启动场景
- 比如spring-boot-starter-web,会自动导入关于web依赖
- springboot会将所有场景功能,变成一个个启动器
- 要使用什么功能,只需要找到对应启动器
主程序
//SpringBootApplication 标注这个类是一个springboot的应用
@SpringBootApplication
public class Springboot01HelloworldApplication {
public static void main(String[] args) {
//将springboot应用启动
SpringApplication.run(Springboot01HelloworldApplication.class, args);
}
}
-
注解
@SpringBootConfiguration : springboot的配置 @Configuration:spring配置类 @Component:说明这也是一个spring组件 @EnableAutoConfiguration:自动配置 @AutoConfigurationPackage 自动配置包 @Import({Registrar.class}) 自动配置包注册 @Import({AutoConfigurationImportSelector.class}) 自动配置导入选择 //获取所有配置 List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes); //获取候选的配置 protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you " + "are using a custom packaging, make sure that file is correct."); return configurations; }
1.3、yaml配置详解
#对空格要求严格
#回车号代表是上级的子属性
server:
port: 8081
#对象
student:
name: zhang
age: 22
#行内写法
studentInfo: {name: zhang,age: 33}
#数组
pets:
- cat
- dog
- pig
#数组行内写法
petArr: [cat,dog,pig]
1.4、绑定配置文件值的几种方式
第一种(读取spring配置文件)
@ConfigurationProperties(prefix = "person")
person:
name: 张三{$random.uuid}
age: 22
happy: false
brith: 2020/01/01
第二种(读取指定配置文件)
@PropertySource(value = "classpath:zhang.properties")
public class Person {
//SPEL表达式取出配置文件中的值
@Value("${name}")
name=张三
两种方式对比
ConfigurationProperties | PropertySource | |
---|---|---|
批量注入属性 | 支持 | 不支持,需要@Value一个个注入 |
松散绑定 | 支持 firstName识别first-name | 不支持 |
SpEL | 不支持 | 支持 |
JSR303数据校验 | 支持 | 不支持 |
复杂类型封装 | 支持 | 不支持 |
结论:
- 需要注入很多属性时,且yml与properties都支持时,用ConfigurationProperties较好
- 注入单个属性时,@Value的方式较好
- 如果单独编写了一个配置文件,用PropertySource较好
1.5、JSR303
核心类
用法
@Validated
public class Person {
//SPEL表达式取出配置文件中的值
// @Value("${name}")
@Email(message = "邮箱错误")
1.6、多环境配置
properties多环境配置
application.properties
#多环境配置
spring.profiles.active=dev
application-dev.properties
server.port=8081
application-test.properties
server.port=8082
yml多环境配置
application.yml
spring:
profiles:
active: dev #环境
application-dev.yml
server:
port: 8084
application-test.yml
server:
port: 8082
1.7、自动装配原理再解析
个人理解:配置文件中的属性,都是一个个的类组件,Springboot将这些类装配管理。在配置文件设置属性后,直接影响类中的属性。
1.8、静态资源导入方式
webjars方式导入:
在WebJars - Web Libraries in Jars中找到资源包,然后在maven中引入,如导入jquery
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>jquery</artifactId>
<version>3.6.0</version>
</dependency>
使用:localhost:8080/webjars/jquery/3.6.0/jquery.js
文件方式导入:
在resources下,public/static/resources都是可以被web访问的,访问优先级rescourse>static(默认使用)>public
使用:localhost:8080/*
1.9、thymeleaf模板引擎
官网文档:https://www.thymeleaf.org/
引入maven
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.14.RELEASE</version>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
Controller
@RequestMapping("/index")
public String index() {
return "index";
}
templates下创建index.html
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
语法
使用前,需要在html中引入
<html xmlns:th="http://www.thymeleaf.org">
纯文本方式一
<span th:text="${msg}"></span>
纯文本方式二
[[${msg}]]
可解析Html的文本
<span th:utext="${msg}"></span>
循环
model.addAttribute("userList", Arrays.asList("zhang", "san"));
<div th:each="user:${userList}" th:text="${user}"></div>
<div th:each="user:${userList}">[[${user}}]]</div>
if条件判断
<span th:if="${msg} == '<h1>ok</h1>'">
true
</span>
<span th:if="${msg} != '<h1>ok</h1>'">
false
</span>
引入文件
th:fragment="sidebar" 标记块
th:insert="dashboard" 引入文件的标记块
th:replace="~{commons/commons::sidebar(active='main.html')}" 也是引入,可以传参
1.10、国际化
-
Settings->File Encodings的三个编码要改成UTF-8
-
在resources下新建i18n文件夹,再新建login.properties、login_zh_CN.properties、login_en_US.properties。
-
配置多语言
login.properties
login.password=密码 #默认
login_zh_CN.properties
login.password=密码 #中文
login_en_US.properties
login.password=password #英文
-
在application.properties中配置多语言
#多语言路径 spring.messages.basename=i18n.login
-
测试
<li><input name="" type="text" class="loginpwd" th:value="#{login.password}" onclick="JavaScript:this.value=''"/>
-
语言切换前端
<div class="loginbm"> <a href="login.html?l=zh_CN">中文</a> <a href="login.html?l=en_US">英文</a> </div>
-
后端解析语言切换
MyLocaleResolver.java 自定义组件
package com.zhang.config; import org.springframework.web.servlet.LocaleResolver; import org.thymeleaf.util.StringUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Locale; public class MyLocaleResolver implements LocaleResolver { //解析请求 @Override public Locale resolveLocale(HttpServletRequest request) { //获取请求中的语言参数 String language = request.getParameter("l"); //如果没有就使用默认的 Locale locale = Locale.getDefault(); //如果请求带了国际化的参数 if (!StringUtils.isEmpty(language)){ //zh_CN String[] s = language.split("_"); //国家,地区 return new Locale(s[0], s[1]); } return locale; } @Override public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { } }
MyMvcConfig.java 将组件配置到Spring容器中
package com.zhang.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Locale;
/**
* @author zhangxiny
* @apiNote
* @date 2022-01-14 17:10
*/
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
//自定义的国际化组件就生效了
@Bean
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}
}
1.11、登录拦截器
自定义拦截器 LoginHandlerInterceptor.java
package com.zhang.config;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class LoginHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
Object userInfo = session.getAttribute("userInfo");
if (userInfo!=null) {
return true;
}
request.setAttribute("msg", "没有权限,请先登录");
request.getRequestDispatcher("/login").forward(request, response);
return false;
}
}
MyMvcConfig.java 将拦截器重写到Spring容器中
package com.zhang.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Locale;
/**
* @author zhangxiny
* @apiNote
* @date 2022-01-14 17:10
*/
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
//配置拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**").excludePathPatterns("/login", "/user/login", "/login.html", "/css/**", "/js/**", "/images/**");
}
}
1.12、Druid
-
引入包
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency>
-
配置后台监控
config/DruidConfig.java
package com.zhang.config; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import javax.servlet.Servlet; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * @author zhangxiny * @apiNote * @date 2022-01-19 15:26 */ @Configuration public class DruidConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource() { return new DruidDataSource(); } /** * 后台监控 * @return ServletRegistrationBean */ @Bean public ServletRegistrationBean statViewServlet() { ServletRegistrationBean<Servlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); //后台需要有人登录,账号密码配置 Map<String, String> map = new HashMap<String, String>(3); //增加配置 map.put("loginUsername", "root"); map.put("loginPassword", "root"); //允许谁可以访问 map.put("allow", ""); bean.setInitParameters(map); return bean; } }
-
打开Druid面板
1.13、整合mybatis
-
导入包
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency>
-
在springboot配置文件中配置mybatis
spring.datasource.username=root spring.datasource.password=root spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #mybatis mybatis.type-aliases-package=com.zhang.pojo mybatis.mapper-locations=classpath:mapping/*.xml
1.14、SpringSecurity
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架
-
引入包(springboot版本2.0.9.RELEASE)
<!--SpringSecurity--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.1.6.RELEASE</version> </dependency> <!--security与thymeleaf的整合--> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> <version>3.0.4.RELEASE</version> </dependency>
-
配置访问权限及登录认证权限 com.zhang.config.SecurityConfig.java
package com.zhang.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { //首页所有人可以访问,功能页只有对应的用户可以访问 @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/") .permitAll() .antMatchers("/level1/**").hasRole("vip1") .antMatchers("/level2/**").hasRole("vip2") .antMatchers("/level3/**").hasRole("vip3"); //没有权限默认跳转到登录页 http.formLogin(); //开启注销并指定跳转页面 http.logout().logoutSuccessUrl("/"); } //认证 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()) .withUser("zhangsan").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2", "vip3") .and() .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2", "vip3") .and() .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1"); } }
-
html页面
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.w3.org/1999/xhtml"> <!--如果未登录--> <a sec:authorize="!isAuthenticated()" th:href="@{/login}">登录</a> <!--如果登录,用户名--> <span sec:authorize="isAuthenticated()" > <a th:href="@{/logout}"> 用户名:<span sec:authentication="name"></span> 注销 </a> </span> <ul sec:authorize="hasRole('vip1')"> <li><a href="/level1/1">level1-1</a></li> <li><a href="/level1/2">level1-2</a></li> <li><a href="/level1/3">level1-3</a></li> </ul> <ul sec:authorize="hasRole('vip2')"> <li><a href="/level2/1">level2-1</a></li> <li><a href="/level2/2">level2-2</a></li> <li><a href="/level2/3">level2-3</a></li> </ul> <sec:authorize url="/level3/"> <ul> <li><a href="/level3/1">level3-1</a></li> <li><a href="/level3/2">level3-2</a></li> <li><a href="/level3/3">level3-3</a></li> </ul> </sec:authorize> </html>
1.15、Shiro
SpringBoot整合Shrio、环境搭建、登录拦截、用户认证、整合Mybatis、请求授权实现、整合Thymeleaf,详见压缩包。
https://zxy-me.oss-cn-beijing.aliyuncs.com/note/springboot-08-shiro.zip
测试环境步骤:
-
导入依赖
-
配置shiro.ini
-
运行Quickstart
1.16、Swagger
-
引入依赖
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency>
-
编写配置类
package com.zhang.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.core.env.Profiles; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.service.VendorExtension; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import java.util.ArrayList; import static springfox.documentation.service.ApiInfo.DEFAULT_CONTACT; /** * @author zhangxiny * @apiNote * @date 2022-02-08 10:28 */ @Configuration @EnableSwagger2 //开启swagger2 public class SwaggerConfig { //配置了swagger的docket bean实例 @Bean public Docket docket(Environment environment) { //设置要显示的dev环境 Profiles profiles = Profiles.of("dev"); boolean flag = environment.acceptsProfiles(profiles); return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) //是否启动swagger .enable(flag) .select() //RequestHandlerSelectors 配置要扫描接口的方式 //basePackage 指定要扫描的包(通常用这个) //any 扫描全部 //none 不扫描 //withClassAnnotation 扫描类上的注解 RestController.class //withMethodAnnotation 扫描方法上的注解 GetMapping.class .apis(RequestHandlerSelectors.basePackage("com.zhang.controller")) //不扫描某个包 //.paths(PathSelectors.ant("/zhang/**")) .build(); } //配置swagger信息 private ApiInfo apiInfo(){ Contact contact = new Contact("Zhang", "http://www.zhangxiny.com", "123456@qq.com"); return new ApiInfo( "API接口文档", "备注说明", "1.0", "urn:tos", contact, "Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0", new ArrayList<VendorExtension>()); } }
-
分组与接口注释
-
分组
@Bean public Docket docket1(){ return new Docket(DocumentationType.SWAGGER_2).groupName("A"); } @Bean public Docket docket2(){ return new Docket(DocumentationType.SWAGGER_2).groupName("B"); } @Bean public Docket docket3(){ return new Docket(DocumentationType.SWAGGER_2).groupName("C"); }
-
API注释
@ApiOperation("测试3") @PostMapping("/hello3") public User hello3(User user){ return user; }
-
参数注释
@ApiOperation("hello2") @GetMapping("/hello2") public String hello2(@ApiParam("用户名") String username){ return "hello2" + username; }
-
实体类及参数注释
@ApiModel("用户实体类") public class User { @ApiModelProperty("用户名") public String username; @ApiModelProperty("密码") public String password; }
-
总结:
- 文档实时更新
- 可以在线测试
1.17、异步任务(不阻塞)
-
springboot启动类开启异步任务
//开启 @EnableAsync @SpringBootApplication public class Springboot09TestApplication { public static void main(String[] args) { SpringApplication.run(Springboot09TestApplication.class, args); } }
-
给需要使用的方式加上注解
@Async public void hello(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("数据正在处理中"); }
1.18、邮件发送
-
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
-
配置
spring.mail.username=123@qq.com spring.mail.password=sjntvltpkdyudagc(邮箱服务商的设置中得到smtp密钥) spring.mail.host=smtp.qq.com #开启加密验证 spring.mail.properties.mail.smtp.ssl.enable=true
-
测试类
@Test void contextLoads() { //简单邮件 SimpleMailMessage simpleMailMessage = new SimpleMailMessage(); simpleMailMessage.setSubject("标题"); simpleMailMessage.setText("内容"); simpleMailMessage.setTo("123@qq.com"); simpleMailMessage.setFrom("123@qq.com"); mailSender.send(simpleMailMessage); } @Test void contextLoads2() throws MessagingException { //复杂邮件 MimeMessage mimeMessage = mailSender.createMimeMessage(); //组装 MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); //正文 helper.setSubject("邮件标题哦"); helper.setText("<p style='color:red;'>邮件正文内容哦</p>", true); //附件 helper.addAttachment("a.doc", new File("I:\\a.doc")); helper.addAttachment("b.doc", new File("I:\\b.doc")); helper.setTo("123@qq.com"); helper.setFrom("123@qq.com"); mailSender.send(mimeMessage); }
1.19、定时任务
-
启动类开启注解
//开启 @EnableScheduling @SpringBootApplication public class Springboot09TestApplication { public static void main(String[] args) { SpringApplication.run(Springboot09TestApplication.class, args); } }
-
测试类
package com.zhang.service; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @Service public class ScheduledService { //在一个特定的时间执行这个方法 //cron表达式 //秒 分 时 日 月 //每天20点22分执行一次 //@Scheduled(cron = "0 22 20 * * ?") //每两秒执行一次 @Scheduled(cron = "0/2 * * * * ?") public void hello(){ System.out.println("这个方法被执行了"); } }
1.20、springboot集成redis
-
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
添加配置
#配置redis spring.redis.host=127.0.0.1 spring.redis.port=6379
-
测试
@Test void contextLoads() { // redisTemplate.opsForValue(); //操作字符串 // redisTemplate.opsForList(); //操作数组 // redisTemplate.opsForSet(); // redisTemplate.opsForHash(); // redisTemplate.opsForZSet(); // redisTemplate.opsForGeo(); // redisTemplate.opsForHyperLogLog(); //获取redis连接 // RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); // connection.flushDb(); // connection.flushAll(); redisTemplate.opsForValue().set("key", "张三"); System.out.println(redisTemplate.opsForValue().get("key")); }
1.21、分布式系统理论
将服务部署到多个主机上,使用nginx代理服务器转发到具体的主机,主机的数据存储、缓存,可以用云数据库、云redis实现。
1.22、Zookeeper + dubbo-admin实现服务注册、调用与监控
-
zookeeper
-
在官网下载:Apache ZooKeeper
-
下载后,conf目录下zoo_sample.cfg文件名改成zoo.cfg
-
在bin中使用cmd启动zkServer.cmd
-
-
dubbo-admin(监控中心)
-
在github获取源码:GitHub - apache/dubbo-admin: The ops and reference implementation for Apache Dubbo
-
下载后,在根目录运行mvn clean package -Dmaven.test.skip=true 打包成jar
-
运行jar后,使用7001端口查看服务信息
-
-
编写服务提供者与消费者两个demo
1.22、总结
-
Spring
IOC:Spring集中管理对象,使用时不需要再初始化,直接使用注解注入即可。
AOP:给原来的业务增加功能,但代码逻辑不会对原业务有任何影响,实现了解耦合,如日志等。
-
微服务
当用户量非常大,一台服务器无法应对压力时。可以将程序拆分成多个服务,每个服务互不影响,并可以部署在不同的服务器上。
技术:
Spring Cloud NetFlix
Apache Dubbo zookeeper
Spring Cloud Alibaba
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用