SpringBoot学习(个人博客项目)
SpringBoot教程
源码来自杨洋的《Spring Boot 2实战之旅》
源码下载链接
1.后台实体
创建对应的实体类,使用JPA进行操作,可以在数据库中生成对应的表
1.文章表
package com.example.demo.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
@Entity
@Table(name="article")
//当实体类与其映射的数据库表名不同名时需要使用 @Table 标注说明,该标注与 @Entity 标注并列使用,置于实体类声明语句之前,可写于单独语句行,也可与声明语句同行。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Article implements Serializable {
private static final long serialVersionUID = 4967006908141911451L;
@Id //表明这是数据库中的主键
@GeneratedValue(strategy = GenerationType.IDENTITY) //表明此字段自增长
private Long articleId;
private String articleName;
@Lob
//指定持久属性或字段应作为大对象持久保存到数据库支持的大对象类型
//Lob类型是根据持久性字段或属性的类型推断的,除了字符串和基于字符的类型以外,默认为Blob。
//String类的默认值为longtext
@Column(columnDefinition = "TEXT") //@Column用于指定持久化属性映射到数据库表的列。如果没有指定列注释,则使用其默认值,columnDefinition可以指定创建表时一些SQL语句,此处表示字段类型为text
private String articleContent;
private String articleAuthors;
@Temporal(TemporalType.DATE)
//如果在某类中有Date类型的属性,数据库中存储可能是'yyyy-MM-dd hh:MM:ss'要在查询时获得年月日,在该属性上标注@Temporal(TemporalType.DATE) 会得到形如'yyyy-MM-dd' 格式的日期。
private Date articleInputDate;
private Integer articleReadingTime;
private Integer isTop;
private Integer isEnable;
/*
@ManyToMany
作用:用于映射多对多关系
属性:
cascade:配置级联操作。
fetch:配置是否采用延迟加载。
targetEntity:配置目标的实体类。映射多对多的时候不用写。
@JoinTable
作用:针对中间表的配置
属性:
nam:配置中间表的名称
joinColumns:中间表的外键字段关联当前实体类所对应表的主键字段
inverseJoinColumn:中间表的外键字段关联对方表的主键字段
@JoinColumn
作用:用于定义主键字段和外键字段的对应关系。
属性:
name:指定外键字段的名称
referencedColumnName:指定引用主表的主键字段名称
unique:是否唯一。默认值不唯一
nullable:是否允许为空。默认值允许。
insertable:是否允许插入。默认值允许。
updatable:是否允许更新。默认值允许。
columnDefinition:列的定义信息。
*/
@ManyToMany
@JoinTable(name = "articleTag", joinColumns = {@JoinColumn(name = "articleId")}, inverseJoinColumns = {@JoinColumn(name = "tagId")})
private List<Tag> tagList;
//项目内使用,非数据库字段
@Transient
//ava 的transient关键字的作用是需要实现Serilizable接口,
// 将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中
private int imageNo;
@Transient
private String articleIntroduction;
@Transient
private String articleShowContent;
}
2.博客页面主功能
1.config:配置,例子中用于配置拦截器
2.constants:常量,例子中常量类所在包
3.controller:控制层,案例中控制层大多只用于封装数据和页面跳转
4.entity:实体类
5.init:初始化,用于初始化应用中的数据
5.interceptors:拦截器,案例中用于更新博客访问次数和初始化底部数据
7.repository:数据操作层,用于JPA操作数据库
8.service:业务层,案例中用于调用数据操作层和一些业务逻辑处理
9.timer:定时器,案例中的定时器主要用于在每日凌晨创建博客访问数据表记录
10.util:工具,案例中包含两个工具类:Markdown转换工具类和去除HTML标签工具类
文章博客页功能实现
后端
1.在repository包下创建ArticleRepository接口
public interface ArticleRepository extends JpaRepository<Article, Long>, JpaSpecificationExecutor<Article> {
Page<Article> findAllByIsTopAndIsEnable(Integer isTop, Integer isEnable, Pageable pageable);
Article findByIsEnableAndArticleId(Integer isEnable, Long articleId);
Page<Article> findAllByIsEnable(Integer isEnable, Pageable pageable);
}
/*JpaRepository是Repository的 JPA 特定扩展。 它包含CrudRepository和PagingAndSortingRepository的完整 API。 因此,它包含用于基本 CRUD 操作的 API,以及用于分页和排序的 API。*/
/*JpaSpecificationExecutor,帮我们提供了一个高级的入口和结构,通过这个入口可以使用底层JPA的Criteria的所有方法,可以满足所有业务场景*/
//JpaRepository用于实现相对简单的查询,而JpaSpecificationExecutor可以满足一切业务需要
//Page和Pageable是Spring Data库中定义的分页工具
/*
1.Page接口: 用于存储查询的结果集
2.Pageable接口:是所有分页相关信息的一个抽象,通过该接口,我们可以得到和分页相关所有信息(例如pageNumber、pageSize等),这样,Jpa就能够通过pageable参数来得到一个带分页信息的Sql语句。
*/
2.在service包下创建AritcleService接口
public interface ArticleService {
List<Article> findIsTopArticleList();
Page<Article> findBlogArticleList(int page, int size);
Page<Article> findSearchArticleList(int page, int size, String keyword);
Article findArticleByArticleId(Long articleId);
Article findIsEnableArticleByArticleId(Long articleId);
void saveArticle(Article article);
}
3.创建ArticleService接口的实现类
@Service
public class ArticleServiceImp implements ArticleService {
@Autowired
private ArticleRepository articleRepository;
@Override
@Cacheable(value = "indexPageArticleList")
public List<Article> findIsTopArticleList() {
Pageable pageable= PageRequest.of(0,6, Sort.Direction.DESC,"articleId");
Page<Article> page=articleRepository.findAllByIsTopAndIsEnable(Constants.YES,Constants.YES,pageable);
List<Article> articleList=page.getContent();
for (int i=0; i<articleList.size(); i++){
Article article=articleList.get(i);
article.setImageNo(i+1);
}
return articleList;
}
@Override
@Cacheable(value = "blogArticle", key = "#page")
public Page<Article> findBlogArticleList(int page, int size) {
Pageable pageable=PageRequest.of(page,size,Sort.Direction.DESC,"articleId");
return articleRepository.findAllByIsEnable(Constants.YES,pageable);
}
@Override
public Page<Article> findSearchArticleList(int page, int size, String keyword) {
Pageable pageable=PageRequest.of(page,size,Sort.Direction.DESC,"articleId");
return articleRepository.findAll(this.getWhereClause(keyword),pageable);
}
@Override
public Article findArticleByArticleId(Long articleId) {
Optional<Article> optionalArticle=articleRepository.findById(articleId);
if (optionalArticle.isPresent()){
return optionalArticle.get();
}
return null;
}
@Override
public Article findIsEnableArticleByArticleId(Long articleId) {
return articleRepository.findByIsEnableAndArticleId(Constants.YES, articleId);
}
@Override
public void saveArticle(Article article) {
articleRepository.save(article);
}
//动态生成where语句
private Specification<Article> getWhereClause(String keyword){
return new Specification<Article>() {
@Override
public Predicate toPredicate(Root<Article> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicate=new ArrayList<>();
if (StringUtils.isNotBlank(keyword)){
predicate.add(
criteriaBuilder.and(
criteriaBuilder.or(
criteriaBuilder.like(root.get("articleName"),"%" + keyword + "%"),
criteriaBuilder.like(root.get("articleContent"), "%" + keyword + "%")
)
)
);
}
predicate.add(criteriaBuilder.equal(root.get("isEnable"), Constants.YES));
Predicate[] pre=new Predicate[predicate.size()];
return query.where(predicate.toArray(pre)).getRestriction();
}
};
}
}
//@Service注解用于标注该类属于业务层
//@Autowired注解它可以对类成员变量、方法及构造函数进行标注,让spring完成bean自动装配的工作
/*
依赖注入和控制反转是同一个概念。具体含义是:当某个角色(可能是一个Java实例,调用者)需要另一个角色(另一个Java实例,被调用者)的协助时,在传统的程序设计过程中,通常由调用者来创建被调用者的实例。但在Spring里,创建被调用者的工作不再由调用者来完成,因此称为控制反转;创建被调用者 实例的工作通常由Spring容器来完成,然后注入调用者,因此也称为依赖注入。
*/
//@Cacheable注解
//Spring Cache 是作用在方法上的,其核心思想是,当我们在调用一个缓存方法时会把该方法参数和返回结果作为一个键值对存在缓存中。
//每次调用需要缓存功能的方法时,Spring 会检查指定参数的指定目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
//在使用JpaSpecificationExecutor接口定义的findAll()方法进行复杂查询时,需要借助于Specification接口(getWhereClause方法)并实现toPredicate方法从而实现动态查询
//getWhereClause方法实现的查询语句就是 select from Article where articleName like %keyword% or articleContent like %keyword% and isEnable=1
/*
Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
Optional 类的引入很好的解决空指针异常。
*/
[SpringBoot 缓存之 @Cacheable 详细介绍 ](https://xie.infoq.cn/article/001e0f5ab65fa7dd1484c51e5#:~:text=%40Cacheable 这个注解,用它就是为了使用缓存的。 所以我们可以先说一下缓存的使用步骤: 1、开启基于注解的缓存,使用 %40EnableCaching,标识在 SpringBoot 的主启动类上。 注:这里使用 %40Cacheable 注解就可以将运行结果缓存,以后查询相同的数据,直接从缓存中取,不需要调用方法。)
4.博客页控制层实现
@Controller
public class BlogController {
@Autowired
private ArticleService articleService;
@GetMapping("/blog")
public String blog(Model model){
return this.blog(model, 1);
}
@GetMapping("/blog/{pageNumber}")
public String blog(Model model, @PathVariable Integer pageNumber){
if (pageNumber==null){
pageNumber=1;
}
Page<Article> articlePage=articleService.findBlogArticleList((pageNumber-1)* Constants.defaultPageSize,Constants.defaultPageSize);
List<Article> articleList=articlePage.getContent();
//条件遍历list集合
articleList.forEach(article -> {
String articleIntroduction=HtmlSpirit.delHTMLTag(article.getArticleContent());
article.setArticleIntroduction(articleIntroduction.length() > 100 ? articleIntroduction.substring(0,100) : articleIntroduction);
});
model.addAttribute("articleList", articleList);
model.addAttribute("totalCount", articlePage.getTotalElements());
model.addAttribute("pageNumber", pageNumber);
return "blog";
}
}
//Model的作用是存储动态页面属性,动态页面文件即View可以在Model中获取动态数据,这样就实现了View和Model分离的目的。
前端
用Thymleaf引用后端数据
博客联系页控制器实现
@Controller
public class ContactController {
@GetMapping("/contact")
public String contact(){
return "contact";
}
@Autowired
private MessageService messageService;
@PostMapping("/contact/sendMail")
@ResponseBody
@Transactional(rollbackFor = Throwable.class)
public String sendMail(@RequestBody Message message){
message.setMessageInputDate(new Date());
messageService.saveMessage(message);
return "success";
}
}
//@responseBody注解的作用是将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来返回JSON数据或者是XML
//@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的)
辅助功能
拦截器
拦截器的主要作用是加载底部数据和更新网站访问次数
@Component
public class RequestInterceptors implements HandlerInterceptor {
Logger logger=LoggerFactory.getLogger(RequestInterceptors.class);
@Autowired
private WebsiteAccessService websiteAccessService;
@Autowired
private TagService tagService;
@Autowired
private LinkService linkService;
@Autowired
private WebsiteConfigService websiteConfigService;
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if (modelAndView != null){
ModelMap modelMap=modelAndView.getModelMap();
logger.info("正在更新网站访问次数");
WebsiteAccess websiteAccess=websiteAccessService.getByAccessDateIs(new Date());
websiteAccess.setAccessCount(websiteAccess.getAccessCount()+1);
websiteAccessService.save(websiteAccess);
logger.info("加入底部数据");
//标签列表
modelMap.addAttribute("tagList", tagService.findAll());
//友情链接
modelMap.addAttribute("linkList", linkService.findAllByIsEnable());
modelMap.addAttribute("websiteConfig", websiteConfigService.findWebsiteConfig());
}
}
}
/*
该类用来实现对拦截器的配置
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
RequestInterceptors requestInterceptors;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestInterceptors);
}
}
定时器
@Component
public class WebSiteTimer {
@Autowired
private WebsiteAccessService websiteAccessService;
@Scheduled(cron = "0 0 0 1/1 * ?")
private void updateTodayWebsiteVisits(){
WebsiteAccess websiteAccess=new WebsiteAccess();
websiteAccess.setAccessCount(1);
websiteAccess.setAccessDate(new Date());
websiteAccessService.save(websiteAccess);
}
}