欢迎访问我的个人博客网站:欢迎来到Turnin的个人博客
github开源地址:https://github.com/Re-restart/my_website
前言
2024年年初,从dji实习回来之后,我一直想着拓宽自己的知识边界。在那里我发现虽然大家不用java,但java的代码逻辑架构却非常的深入人心。这让我对java第一次有了想接触学习的想法,也想着什么时候自己能用java写个项目出来,比如说现在这个个人博客网站,前后端都自己写(当然也会参考开源项目和ai),不用wordpress搭建,自己部署并买个域名,做成真正的网站。
其实原先的想法是做一个类似confluence的网站,现在这种,也算还行了哈哈哈
在我看来,java与python是有共通之处的,java中的继承其实就是python中的父子类,封装是python中的命名函数,多态是同一个函数名称可以调用不同的变量,生成不同的结果。这两种语言都是面向对象的语言。
由于2024年上半年做毕业设计,也在实习;2024下半年去读研,忙着上课提升数学能力,因此这个想法一直被搁置,直到现在。
java网站项目和stm32单片机项目的对比思考
用clion搭建stm32开发环境的时候:Linux系统下速通stm32的clion开发环境配置_linux clion stm32-CSDN博客
我发现了maven与cmake的相似之处,都是用来生成java/c的项目,cmake指挥gcc编译项目,maven指挥jar编译项目;并且我的单片机代码结构,和java项目的代码结构也有共通之处:如果用cubemx生成代码,你会发现它给你生成了core文件夹,core文件夹里有inc和src,还有startup,这些都是构造项目的主要函数。正如java网站项目中,src目录下也都是项目的主要执行文件。
但也有不同之处,比如如果想添加自己的函数文件,需要创建new文件夹,因为cubemx生成时会自动更新core文件夹和CMakeLists.txt。但是java项目就不需要,因为所有的配置文件需要自己来写,IDEA只是搭建了最初始的程序框架,也不会主动更新自己src文件夹。
就依赖库来说,STM32作为嵌入式底层系统开发工具,手动管理.h/.c头文件,并链接脚本。java则是自动构建依赖树,开发效率更高。
两个项目都有底层配置和中间件,单片机项目有driver驱动类文件和middlewares中间件,网站项目有底层的jvm,springboot和plugins插件,thymeleaf也是中间件,pox.xml是中间件配置文件。
单片机项目的外部库是arm-gcc配置文件,java项目的外部库是maven编译文件。到最后,单片机项目构建的是elf文件,再由此生成hex文件和bin文件,java构建的是jar文件,通过java运行。
我这个单片机使用的是rtos系统,完成一些多线程任务,与传统裸机项目的前后台循环任务有所不同。这个网站项目运行于jvm,通过线程池等机制完成异步任务,两者也有相似之处。细细想来,两者差距并不算大,rtos与jvm在一定程度上都起到了系统调度的作用
所以现在,是时候捋顺思路,写完这个个人博客网站了。
博客网站需求思路
一个博客网站需求如下:
1.发文章:因为是个人博客网站,只需要博主本人发文,所以可以把文章写成markdown格式,放在resources/markdown文件夹下读取,把markdown文本转换成html文本格式,并把具体内容用详情页展示出来。
因此需要文章列表页面前后端,文章详情页面前后端。
2.文章分类:文章太多,而且需要分类,所以需要文章分类页面前后端。
3.访客留言板:我简化了登陆功能和评论功能,这俩后期也比较难维护,取而代之使用访客留言板,在这个界面里会让访客填写他的昵称和想对我说的话,并在界面上展示。我会通过发文章的形式回复这些留言。需要注意的一点是,这里访客属于匿名发帖,即使一个人也可以起很多昵称。但是同时也会记录访客的留言时间,访问ip地址等内容,所以安全还算有保障。且每次我更新和重新部署网站后,访客留言板数据库都会被清空。
此功能简称为访客留言页面前后端。
4.主要展示页面:主页面包括个人简介和sidebar目录栏,还有中间的照片展示墙和精品文章展示栏。个人简介和sidebar目录栏在这里面是最全面的,之后复制到其他前端页面上。和其他页面不同的地方就是中间的这些内容。照片展示墙用于之后一些单片机/app开源项目/精品文章的宣传(当然也会为了赏心悦目放上像现在主页这样的风景照,都是我自己拿无人机拍的)。
此功能简称为主要展示页面前后端。
博客网站基础设置
这个jdk关乎调试过程中java版本的打包部署,最好选择一样的版本。(不过java的向后兼容特性貌似很强大)
另外需要
Springboot (Spring Web)
Thymeleaf(模板引擎)
Spring Data JPA(数据库)
H2 Database(嵌入式数据库)
由此便能得到基础的java网站代码架构了
博客网站后端
文章相关的后端
文章分类,文章列表和文章详情都可以放在ArticleController类里完成
@GetMapping({"","/"})
//文章列表页
public String articles(Model model) {
try {
Resource[] resources = resourceResolver.getResources("classpath:markdown/*.md");
List<Article> articles = new ArrayList<>();
long idCounter = 1;
for (Resource res : resources) {
String filename = res.getFilename(); // xxx.md
String[] parts = null;
if (filename != null) {
parts = filename.split("_", 2);
}
String category = null;
if (parts != null) {
category = parts[0];
}
String title = null;
if (parts != null) {
title = parts[1].replace(".md", "");
}
Article article = new Article();
article.setId(idCounter++); // 用假的 ID,实际可用 hash 或 UUID
article.setTitle(title);
article.setCategory(category);
article.setContent(""); // 不在列表中显示内容
articles.add(article);
}
// 保存到数据库
model.addAttribute("articles", articles);
} catch (IOException e) {
model.addAttribute("error", "加载文章列表失败");
}
return "articles";
}
// 文章详情页(新增@PathVariable)
@GetMapping("/{category}/{title}")
public String articleDetails(@PathVariable String category, @PathVariable String title, Model model) throws IOException {
String fullFilename = category + "_" + title + ".md";
Resource resource = resourceResolver.getResource("classpath:markdown/" + fullFilename);
if (!resource.exists()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "文章不存在");
}
String content = IOUtils.toString(resource.getInputStream(), StandardCharsets.UTF_8);
HtmlRenderer renderer = HtmlRenderer.builder().build();
String htmlContent = renderer.render(Parser.builder().build().parse(content));
Article article = new Article();
article.setTitle(title);
article.setCategory(category);
article.setContent(htmlContent);
model.addAttribute("article", article);
return "article-details"; // 文章详情模板
}
//文章分类页
@GetMapping("/category/{category}")
public String articlesByCategory(@PathVariable String category, Model model) {
try {
Resource[] resources = resourceResolver.getResources("classpath:markdown/*.md");
List<Article> articles = new ArrayList<>();
long idCounter = 1;
for (Resource res : resources) {
String filename = res.getFilename(); // 例如:随笔_生活记录.md
if (filename != null && filename.contains("_")) {
String[] parts = filename.split("_", 2);
String fileCategory = parts[0];
String title = parts[1].replace(".md", "");
// 只保留匹配当前分类的文章
if (fileCategory.equalsIgnoreCase(category)) {
Article article = new Article();
article.setId(idCounter++);
article.setTitle(title);
article.setCategory(fileCategory);
article.setContent(""); // 列表页不显示内容
articles.add(article);
}
}
}
model.addAttribute("articles", articles);
model.addAttribute("category", category);
} catch (IOException e) {
model.addAttribute("error", "加载文章分类失败");
}
return "category-articles"; // templates/category-articles.html
}
因为有文章分类的需求在,每次都需要读取标题或者显示分类,所以这里居然没有用到articleRepo,多次使用了直接读取markdown里文件的形式。
另外需要建立Article类和ArticleReopsitory接口
@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 100)
private String title;
@Lob // 适合存储长文本
@Column(columnDefinition = "TEXT")
private String content;
private String category;
// 必须添加无参构造器
public Article() {}
// 需要添加setter方法
public void setId(Long id) {
this.id = id;
}
public void setTitle(String title) {
this.title = title;
}
public void setContent(String content) {
this.content = content;
}
public void setCategory(String category) { this.category = category; }
public String getTitle() {return title;}
public String getContent() {return content;}
public String getCategory() { return category; }
}
public interface ArticleRepository extends JpaRepository<Article, Long> {
}
访客留言后端
对于这个功能,我建立了Observer类用于记录访客的nickname和message,同时记录此时的ip地址,发布评论的时间,评论数量等等信息,再通过前端显示已经记录的评论昵称和评论内容。
@GetMapping("/guestbook")
public String guestbook(Model model) {
// 统计留言数量
model.addAttribute("observerCount", observerRepo.count());
model.addAttribute("messages", observerRepo.findAll());
return "guestbook";
}
@PostMapping("/guestbook/add")
public String submitMessage(
@RequestParam String nickname,
@RequestParam String message,
HttpServletRequest request) {
Observer observer = new Observer();
observer.setObserveTime(LocalDateTime.now());
observer.setNickname(nickname);
observer.writeMessage(message); // 使用正确的setter方法
observer.setIpAddress(request.getRemoteAddr()); // 添加IP记录
observer.setMessage(true); // 设置留言标记
observerRepo.save(observer);
return "redirect:/guestbook";
}
当然也需要建立相应的Observer类和ObserverRepository接口,具体可见我在github上开源的代码,网站效果的话可以点链接访问一下。
博客网站前端
这里我借鉴了一个github开源项目,并在他的基础上完成了一些改动:
https://github.com/ZengZhiK/AdminLTE-Blog
我删除了自己用不到的一些功能和相关的html页面,加入了照片墙,显示markdown转html后的文本,文章分类后显示目录等等,完成了自己代码前后端的适配。另外也按照自己的习惯改变了一下边栏页面,比较重要的代码如下:
照片墙功能
<div id="simpleCarousel" class="carousel slide mb-3" data-ride="carousel">
<div class="carousel-inner">
<div class="carousel-item active">
<img th:src="@{/img/wallpaper4.png}" class="d-block w-100" style="height: 550px; object-fit: cover;" alt="图片1">
</div>
<div class="carousel-item">
<img th:src="@{/img/wallpaper.jpg}" class="d-block w-100" style="height: 550px; object-fit: cover;" alt="图片2">
</div>
<div class="carousel-item">
<img th:src="@{/img/wallpaper7.png}" class="d-block w-100" style="height: 550px; object-fit: cover;" alt="图片3">
</div>
</div>
<a class="carousel-control-prev" href="#simpleCarousel" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
</a>
<a class="carousel-control-next" href="#simpleCarousel" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
</a>
</div>
显示文章详情
<div class="card-header">
<h3 class="card-title">文章详情</h3>
</div>
<div class="card-body">
<div class="markdown-body" th:utext="${article.content}"></div>
</div>
显示所有文章
<div class="card-header">
<h3 class="card-title">所有文章</h3>
</div>
<div class="card-body">
<div class="list-group">
<a th:each="article : ${articles}"
th:href="@{'/articles/' + ${article.category} + '/' + ${article.title}}"
class="list-group-item list-group-item-action">
<span th:text="${article.title}"></span>
</a>
</div>
<div th:if="${error}" class="alert alert-danger mt-3" th:text="${error}"></div>
</div>
显示分类后的文章
<div class="card">
<div class="card-header">
<h3 class="card-title">分类后的文章</h3>
</div>
<div class="card-body">
<div class="list-group">
<a th:each="article : ${articles}"
th:href="@{'/articles/' + ${article.category} + '/' + ${article.title}}"
class="list-group-item list-group-item-action">
<span th:text="${article.title}"></span>
</a>
</div>
<div th:if="${error}" class="alert alert-danger mt-3" th:text="${error}"></div>
</div>
</div>
显示评论后的内容
<div class="direct-chat-messages">
<div th:each="observer : ${messages}" class="direct-chat-msg">
<div class="direct-chat-infos clearfix">
<span class="direct-chat-name float-right" th:text="${observer.nickname}"></span>
<span class="direct-chat-timestamp float-right ml-2" th:text="${#temporals.format(observer.observeTime, 'yyyy-MM-dd HH:mm')}"></span>
</div>
<img class="direct-chat-img position-absolute broader" src="/upload/user.png" alt="头像">
<div class="direct-chat-text float-right" th:text="${observer.message}"></div>
</div>
</div>
另外的一些配置
就是去配置pox.xml和application.properties,可以理解为pom.xml是中间件的配置文件,而application.properties是整个网站的配置文件,端口和数据库控制台都是在这里设置。
最终效果
网站的部署
在本地实验完,需要部署到服务器,外面才能访问的到。我这里使用了腾讯云的服务器,买个最便宜的就可以,系统我这里安装的是ubuntu 24.04。
有两种部署方式,第一种是docker容器化部署(这个容易遇到因为网络,无法拉取openjdk镜像的问题),第二种是直接部署。
docker容器化部署
在demo文件夹下新建一个dockerfile文件(试着用docker_compose.yml了,相比之下它更适合部署操作系统本身,网站这种dockerfile就很合适)
# 使用官方轻量 JDK 镜像
FROM openjdk:17-jdk-alpine
# 创建工作目录
WORKDIR /app
# 复制 jar 文件
COPY target/demo-0.0.1-SNAPSHOT.jar app.jar
# 默认执行命令
ENTRYPOINT ["java", "-jar", "app.jar"]
在命令行内运行
./mvnw clean package //构建jar包
//把demo文件夹用ftp方式传输到云服务器上,本机需要下载filezilla
//转到服务器平台上,在服务器平台安装docker后
sudo docker build -t my_website . //对文件夹里的所有文件完成docker镜像构建,my_website是构建镜像的名字
docker run -d -p 8080:8080 --name java_web my_website
//这个项目springboot默认监听8080端口,docker容器名叫java_web
直接部署
这个非常简单:
./mvnw clean package
java -jar target/demo-0.0.1-SNAPSHOT.jar
输入域名/公网地址就可以访问网站
此时你会发现只能通过8080端口访问到网站,和大多数网站还不一样。为了能用默认的80端口访问到网站(也就是不输入端口号),需要改变application.properties或者配置nginx反向代理。
spring.application.name=demo
# ??H2???
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# ??testdb??
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.jpa.hibernate.ddl-auto=update
# Thymeleaf??
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
# ????????
server.port=80
spring.jpa.properties.hibernate.retry_attempts=3
spring.jpa.properties.hibernate.retry_interval=1000
//会提示权限不够,这个时候需要在demo命令行环境下
./mvnw clean package
java -jar target/demo-0.0.1-SNAPSHOT.jar
如果不想这么做,就要在服务器里安装nginx并修改配置文件
sudo apt update
sudo apt install nginx
sudo nano /etc/nginx/sites-available/default
找到这个配置并修改
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location ~* \.(css|js|jpg|jpeg|png|gif|ico|woff|ttf|svg)$ {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
然后检查配置并重启nginx
sudo nginx -t //检查配置是否有错
sudo systemctl restart nginx
现在就成功部署好网站了!