首页 > IT职业 > IT职场 > Java博客系统实战:从零搭建
在当今数字化时代,拥有一个个人博客是展示专业能力、分享知识的重要途径。本文将带您从零开始,使用Java技术栈构建一个功能完善的博客系统。我们将采用Spring Boot框架、MySQL数据库和Thymeleaf模板引擎,打造一个具备用户管理、文章发布、评论互动等核心功能的博客平台。
textCopy Code├── com.example.blog │ ├── config # 配置类 │ ├── controller # 控制器层 │ ├── model # 数据模型 │ ├── service # 业务逻辑层 │ ├── repository # 数据访问层 │ ├── security # 安全相关 │ └── web # 前端资源
javaCopy Code@Entity@Table(name = "users")@Getter @Setter@NoArgsConstructor@AllArgsConstructor@EqualsAndHashCode@ToString@Builder@NoArgsConstructor@AllArgsConstructor@Builder(toBuilder = true)public class User implements Serializable { @Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "username", unique = true, nullable = false, length = 50)
private String username;
@Column(name = "password", nullable = false, length = 255)
private String password;
@Column(name = "email", unique = true, nullable = false, length = 100)
private String email;
@Column(name = "role", nullable = false)
private String role;
@Column(name = "created_at", insertable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Post> posts = new ArrayList<>();
}
javaCopy Code@Entity@Table(name = "posts")@Getter @Setter@NoArgsConstructor@AllArgsConstructor@EqualsAndHashCode@ToString@Builder@NoArgsConstructor@AllArgsConstructor@Builder(toBuilder = true)public class Post { @Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "title", nullable = false, length = 200)
private String title;
@Column(name = "content", nullable = false, columnDefinition = "TEXT")
private String content;
@Column(name = "slug", unique = true, nullable = false, length = 200)
private String slug;
@Column(name = "status", nullable = false)
private String status;
@Column(name = "created_at", insertable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@ManyToOne
@JoinColumn(name = "author_id", referencedColumnName = "id")
private User author;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
}
javaCopy Code@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/about", "/contact", "/post/‌**", "/category/**‌", "/tag/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutSuccessUrl("/")
.permitAll()
)
.sessionManagement(session -> session
.sessionFixation().migrateSession()
.invalidSessionUrl("/login?error")
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() { return new UserDetailsServiceImpl(userRepository);
}
}
PostController.java
javaCopy Code@RestController@RequestMapping("/api/posts")@RequiredArgsConstructorpublic class PostController {
private final PostService postService;
@GetMapping
public List<Post> getAllPosts(@RequestParam(required = false) String status) { return postService.getPostsByStatus(status);
}
@GetMapping("/{id}")
public Post getPost(@PathVariable Long id) { return postService.getPostById(id);
}
@PostMapping
public ResponseEntity<Post> createPost(@RequestBody Post post) { Post savedPost = postService.createPost(post); return ResponseEntity.ok(savedPost);
}
@PutMapping("/{id}")
public ResponseEntity<Post> updatePost(@PathVariable Long id, @RequestBody Post post) { Post updatedPost = postService.updatePost(id, post); return ResponseEntity.ok(updatedPost);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deletePost(@PathVariable Long id) {
postService.deletePost(id); return ResponseEntity.noContent().build();
}
}
PostService.java
javaCopy Code@Service@RequiredArgsConstructorpublic class PostService {
private final PostRepository postRepository;
public List<Post> getPostsByStatus(String status) { if (status == null) { return postRepository.findAll();
} return postRepository.findByStatus(status);
}
public Post getPostById(Long id) { return postRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Post not found"));
}
public Post createPost(Post post) {
post.setCreatedAt(LocalDateTime.now());
post.setUpdatedAt(LocalDateTime.now()); return postRepository.save(post);
}
public Post updatePost(Long id, Post post) { Post existingPost = postRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Post not found"));
existingPost.setTitle(post.getTitle());
existingPost.setContent(post.getContent());
existingPost.setSlug(post.getSlug());
existingPost.setStatus(post.getStatus());
existingPost.setUpdatedAt(LocalDateTime.now());
return postRepository.save(existingPost);
}
public void deletePost(Long id) {
postRepository.deleteById(id);
}
}
htmlCopy Code<!DOCTYPE html><html xmlns:th="http://www.thymeleaf.org"><head>
<meta charset="UTF-8">
<title>Blog - 文章列表</title>
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/css/style.css}"></head><body><div class="container">
<div class="row">
<div class="col-md-8">
<h1 class="mb-4">所有文章</h1>
<div th:if="${#lists.size(posts) == 0}">
<p>暂无文章</p>
</div>
<div th:each="post : ${posts}" th:class="post">
<div class="card mb-4">
<div class="card-body">
<h2 class="card-title"><a th:href="@{/post/} + ${post.slug}" th:text="${post.title}"></a></h2>
<p class="card-text" th:text="${#strings.truncate(post.content, 200)}"></p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<a th:href="@{/post/} + ${post.slug}" class="btn btn-primary">阅读更多</a>
</div>
<small th:text="${#dates.format(post.createdAt, 'yyyy-MM-dd')}"></small>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<nav>
<ul class="pagination">
<li th:class="${pageNumber == 1} ? 'disabled' : ''" th:classappend="${pageNumber == 1} ? 'disabled' : ''">
<a th:href="@{/posts?page=${pageNumber - 1}}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<li th:each="page : ${#numbers.array(pageCount, 1)}" th:class="${pageNumber == page} ? 'active' : ''">
<a th:href="@{/posts?page=${page - 1}}">[[${page}]]</a>
</li>
<li th:class="${pageNumber == pageCount} ? 'disabled' : ''">
<a th:href="@{/posts?page=${pageNumber + 1}}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</nav>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">分类</h5>
<ul class="list-group">
<li class="list-group-item" th:each="category : ${categories}" th:text="${category.name}"></li>
</ul>
</div>
</div>
<div class="card mt-4">
<div class="card-body">
<h5 class="card-title">标签云</h5>
<div class="tag-cloud">
<a th:href="@{/tag/} + ${tag}" th:each="tag : ${tags}" th:text="${tag}" class="btn btn-sm btn-light me-2 mb-2"></a>
</div>
</div>
</div>
</div>
</div></div><script th:src="@{/js/jquery-3.6.0.min.js}"></script><script th:src="@{/js/bootstrap.bundle.min.js}"></script></body></html>
htmlCopy Code<!DOCTYPE html><html xmlns:th="http://www.thymeleaf.org"><head>
<meta charset="UTF-8">
<title th:text="${post.title} + ' - 我的博客'"></title>
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/css/style.css}"></head><body><div class="container">
<div class="row">
<div class="col-md-8">
<h1 th:text="${post.title}"></h1>
<p class="text-muted">
<span th:text="${#dates.format(post.createdAt, 'yyyy-MM-dd')}"></span>
<span th:text="${post.author.username}"></span>
</p>
<div th:utext="${post.content}"></div>
<hr>
<h3>评论</h3>
<div th:each="comment : ${post.comments}" th:class="comment">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title" th:text="${comment.author.username}"></h5>
<p class="card-text" th:text="${comment.content}"></p>
<p class="text-muted" th:text="${#dates.format(comment.createdAt, 'yyyy-MM-dd HH:mm')}"></p>
</div>
</div>
</div>
<h3>发表评论</h3>
<form th:action="@{/post/${post.id}/comment}" method="post">
<div class="mb-3">
<textarea class="form-control" name="content" rows="5" required></textarea>
</div>
<button type="submit" class="btn btn-primary">提交评论</button>
</form>
</div>
<div class="col-md-4">
<!-- 侧边栏内容 -->
</div>
</div></div><script th:src="@{/js/jquery-3.6.0.min.js}"></script><script th:src="@{/js/bootstrap.bundle.min.js}"></script></body></html>
Dockerfile
dockerfileCopy CodeFROM openjdk:11-jre-slim WORKDIR /app COPY target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
docker-compose.yml
yamlCopy Codeversion: '3.8'services: blog: build: . ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=prod depends_on: - db restart: unless-stopped db: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORD=root - MYSQL_DATABASE=blog - MYSQL_USER=user - MYSQL_PASSWORD=password volumes: - db_data:/var/lib/mysql ports: - "3306:3306" restart: unless-stoppedvolumes: db_data:
数据库优化:
缓存策略:
异步处理:
通过本文,我们完整地构建了一个基于Java的博客系统。这个项目涵盖了Spring Boot的核心功能,包括:
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69921587/viewspace-3103436/ ,如需转载,请注明出处,否则将追究法律责任。