系统设计
整个博客系统包括用户管理,安全设置,博客管理,评论管理,点赞管理,分类管理,标签管理和首页搜索。前端使用boostrap,thymeleaf模板引擎,jQuery等等,后端使用springboot,springMVC,spring data,spring security管理安全,数据存储使用MySQL,H2,MongoDB,MongoDB主要是存储文件等等,其他还有ElasticSearch,这次项目就尝试使用Gradle,不用maven了。
用户管理
注册,登录,增加用户,修改用户,删除用户,搜索用户。
安全设置
角色授权,权限设置
博客管理
发表博客,编辑博客,删除博客,分类博客,设置标签, 上传图片,模糊查询,最新排行,最热排序,阅读量统计
评论管理
发表评论,删除评论,统计评论,
点赞管理
点赞,取消点赞,统计
分类管理
创建分类,编辑分类,删除分类,按分类查询
标签管理
创建,删除,查询
首页管理
全文检索,最新文章,最热文章,热门标签,热门用户,热门文章,最新发布
配置环境gradle
这个环境有点奇怪,用idea的spring initial创建会出现找不到spring boot驱动的问题,发现是gradle没有选择global环境,如果没有选择就默认是找本地,本地没有当然报错了。选了globe还有问题,出现了A problem occurs from '项目名',仔细看发现他每一层都报了错,后来尝试着直接从官网配置好了下载下来,然后又是漫长的等待,真的很漫长,不知道是电脑垃圾还是咋地,感觉配置起来是比maven简单,build-gradle里面也比maven好理解,就是等太久了,我开始还以为是死机了。
4分钟才构建好,中途我还以为是电脑垃圾。还是只能直接从官网下载,idea自己spring initial不知道为什么总是出现springbootV2.2.2驱动不能识别的问题。如果这个时候心急了直接点击运行,会出现:Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.很明显是因为数据库没配好(因为我导入的时候把jpa引入了),这个时候按照提示把数据库补上就好了。
项目结构
项目里面有一个build.gradle:
这个文件是整个项目的一个脚本,里面都是gradle语言的语法,respositries里面的mavenCentral()就是指定使用中央仓库。build整个文件就是gradle构建之后生成的,gradle目录里面有一个wrapper,这个东西是可以使其自动下载gradle,wrapper可以省去安装gradle的步骤gradle-wrapper.properties文件就是配置文件,这个文件最后一行:distributionUrl=https://services.gradle.org/distributions/gradle-6.0.1-all.zip
就是gradle的版本,src就是源码了,test测试代码。
测试一下看看行不行吧。创建一个controller类,controller类需要进行Http请求,所以需要MockMvc,注意这个MockMvc需要用AutoConfigureMockMvc注释
代码语言:javascript复制@SpringBootTest
@AutoConfigureMockMvc
class HelloControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void hello() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/hello").accept(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isOk()).andExpect(MockMvcResultMatchers.content().string(Matchers.equalTo("Hello world!")));
}
}
后面那一堆就是测试,请求首先判断状态是不是200,判断是不是返回了hello world。和原来的有些不一样,可能这里没有用到RunWith注解,使用RunWith注解以便在测试开始的时候自动创建Spring的应用上下文,注解了@RunWith就可以直接使用spring容器,直接使用@Test注解,不用启动spring容器,但是这里用是gradle是6版本,不支持Junit4,只支持Junit5。
集成Thymeleaf
变量表达式
{book}">,变量表达式里面是变量
消息表达式
#{...}, <th th:text = "#{header.address}">...</th>,也称为文本外部化,国际化等等。
选择表达式
<div th:object = "${book}"> <span th:text = "*{title}"></span> </div>与变量表达式区别,他们是在当前选择的对象而不是整个上下文变量映射上执行,比如这里的title只是变量book。
链接表达式
@{...}这个没什么好说的,但是种类很多。
分段表达式
<div th:fragent = "copy"> </div> 还有其他的一些比较大小等等。
迭代器
th:each,数组,map,list都可以用迭代器。状态变量,index这些其实就是索引。 实践一下,简单写一个crud用户管理。首先需要有控制器,控制器两个注解,@RestController,这个注解其实就是controller注解和Respondebody这两个注解的集合,还有一个RequestMapping,get,post注解都可以接受。rest风格的注解就用到两个,GetMapping,PostMapping注解。
代码语言:javascript复制@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping
public ModelAndView list(Model model) {
model.addAttribute("userList", userRepository.listUser());
model.addAttribute("title", "用户管理");
return new ModelAndView("users/list", "userModel", model);
}
@GetMapping("/delete/{id}")
public ModelAndView delete(@PathVariable("id") Long id){
userRepository.deleteUser(id);
return new ModelAndView("redirect:/users");
}
@GetMapping("{id}")
public ModelAndView view(@PathVariable("id") Long id, Model model) {
User user = userRepository.getUserById(id);
model.addAttribute("user", user);
model.addAttribute("title", "查看用户");
return new ModelAndView("users/view", "userModel", model);
}
@GetMapping("/modify/{id}")
public ModelAndView modify(@PathVariable("id") Long id, Model model) {
User user = userRepository.getUserById(id);
model.addAttribute("user", user);
model.addAttribute("title", "修改用户");
return new ModelAndView("users/form", "userModel", model);
}
@GetMapping("/form")
public ModelAndView createForm(Model model) {
model.addAttribute("user", new User());
model.addAttribute("title", "创建用户");
return new ModelAndView("users/form", "userModel", model);
}
@PostMapping
public ModelAndView saveOrUpdateUser(User user) {
userRepository.saveOrUpdate(user);
return new ModelAndView("redirect:/users");
}
}
ModelAndView最后返回redirect:/users是重定向,其实就是又回到某个Controller,如果是直接返回users/form就是返回页面。然后就是html页面的编写:
代码语言:javascript复制<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="UTF-8">
<title>Thymeleaf in action</title>
</head>
<body>
<div th:replace="~{fragements/header :: header}"></div>
<h3 th:text="${userModel.title}">greenArrow</h3>
<div>
<a href="/users/form.html" th:href="@{/users/form}">创建用户 </a>
</div>
<table border="1">
<thead>
<tr>
<td>ID</td>
<td>Email</td>
<td>Name</td>
</tr>
</thead>
<tbody>
<tr th:if="${userModel.userList.size()} eq 0">
<td colspan="3">无用户信息</td>
</tr>
<tr th:each="user : ${userModel.userList}">
<td th:text="${user.id}"></td>
<td th:text="${user.email}"></td>
<td><a th:href="@{'/users/' ${user.id}}" th:text="${user.name}"></a></td>
</tr>
</tbody>
</table>
<div th:replace="~{fragements/footer :: footer}"></div>
</body>
</html>
主要就是熟悉几个标签而已,片段th:replace等等。
ElasticSearch
主要就是应用全文检索,建立本库-》建立索引-》执行搜索-》过滤结果。有点像lucene检索,除了ElasticSearch还有solr,也是一个级别的检索,但都是基于lucene来进行实现的。ES高度可扩展的开源全文搜索和分析引擎,快速的,近实时的对大数据进行存储,搜索和分析。ES特点:首先他是分布式,会把内容分开到多个分片上, 高可用,多类型。近实时:就是接近实时而不是正在的实时,在搜索和可搜索文档之间延时1s,Lucene是可以做到的,这种实时要不是牺牲索引效率,每次索引都要刷新一次,要么牺牲查询效率,每次查询前都要进行刷新,而ES是固定时间刷新一次,一般设置一秒。索引建立之后不会直接写入磁盘,而是通过刷新同步到磁盘里面去。集群,就是一个或者多个节点的集合,保存应用的全部数据,提供基于全部节点的集成似的索引功能,默认就是index elaticsearch。索引,每个索引都有一个名称,通过这个名称可以对文档进行crud操作,单个集群可以定义容易数量的索引。分片,企业中索引存储比较大,一般会超过单个索引节点所能处理的范围,而ES是可以分片也可以聚合,对于分片数据还要建立一个副本。
集成springboot和ElasticSearch
首先需要一个ES服务器,需要spring data es的支持,还需要JNA的一个库。
定义一个pojo,作为索引,而在es中索引的最小单位是 document文档,所以这个类要设置成document:
代码语言:javascript复制@Document(indexName = "blog", type = "blog")
public class EsBlog implements Serializable {
@Id
private String id;
private String title;
private String summary;
private String content;
protected EsBlog() {
}
public EsBlog(String title, String summary, String content) {
this.title = title;
this.summary = summary;
this.content = content;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "EsBlog{"
"id='" id '''
", title='" title '''
", summary='" summary '''
", content='" content '''
'}';
}
}
然后建立索引repository:
代码语言:javascript复制public interface EsBlogRepository extends ElasticsearchRepository<EsBlog, String> {
Page<EsBlog> findDistinctEsBlogByTitleContainingOrSummaryContainingOrContentContaining(String title, String summary, String Content, Pageable pageable);
}
还要下载elasticsearch,Mac不需要安装,直接在bin目录下打开运行sh ./elasticsearch,如果打开localhost:9200在浏览器出现:
9200端口是HTTP访问的端口,而9300端口是jar或者集群之间的通信端口,所以如果浏览器访问需要9200端口,而在gradle中配置需要配置9300端口:
写一个简单的测试用例:
代码语言:javascript复制@SpringBootTest
class EsBlogRepositoryTest {
@Autowired
private EsBlogRepository esBlogRepository;
@BeforeEach
public void initRepository() {
esBlogRepository.deleteAll();
esBlogRepository.save(new EsBlog("asxaxs", "qsqdwq", "kswwqd"));
esBlogRepository.save(new EsBlog("awews喂喂喂xaxs", "qsdwfwqdwq", "kswwqw我弟弟qd"));
esBlogRepository.save(new EsBlog("w驱蚊器无eweasxa", "qwewfesqdwq", "wefwefkssdsfwwqd"));
}
@Test
void findDistinctEsBlogByTitleContainingOrSummaryContainingOrContentContaining() {
Pageable pageable = PageRequest.of(0, 20);
String title = "xs";
String summary = "喂喂";
String content = "wjke";
Page<EsBlog> page = esBlogRepository.findDistinctEsBlogByTitleContainingOrSummaryContainingOrContentContaining(title, summary, content, pageable);
org.assertj.core.api.Assertions.assertThat(page.getTotalElements()).isEqualTo(2);
for (EsBlog blog : page.getContent()) {
System.out.println(blog.toString());
}
}
}
运行前肯定需要先初始化es,所以加上一个@BeforeEach,Junit4用@Before,BeforeEach是Junit5。 接下来完成一下控制层,刚刚基本服务已经测试过了:
代码语言:javascript复制@RestController
@RequestMapping("/blogs")
public class BlogController {
@Autowired
private EsBlogRepository esBlogRepository;
@GetMapping("/list")
public List<EsBlog> list(@RequestParam(value = "title") String title,
@RequestParam(value = "summary") String summary,
@RequestParam(value = "content") String content,
@RequestParam(value = "pageIndex", defaultValue = "0") int pageIndex,
@RequestParam(value = "pageSize", defaultValue = "10") int pageSize) {
Pageable pageable = PageRequest.of(pageIndex, pageSize);
Page<EsBlog> page = esBlogRepository.findDistinctEsBlogByTitleContainingOrSummaryContainingOrContentContaining(title, summary, content, pageable);
return page.getContent();
}
}
使用postman测试一下:
架构设计与分层
如果一个架构不分层,那么就可能存在JSP访问数据库的例子,代码不清晰,难以维护,而且这种各方都混杂在一起职责不够清晰,代码也没有分工。博客系统职责划分,博客系统分为了两套系统,纯粹的博客系统和文件管理系统,文件管理比如图片等等吧,这也是两个主要核心子系统,博客系统需要的肯定是关系型数据库,比如MySQL,Oracle,当然也有非关系型,比如ES就是;而文件系统就一般用nosql了,比如MongoDB,通过restful api进行交互。
spring security
核心领域的概念
- 认证:认证是建立主体的过程,主体通常是可以在应用程序中执行操作的用户,设备或其他系统,不一定是人。
- 授权:或称为访问控制,授权是指是否允许在应用程序中执行操作。 首先进行安全配置。 需要创建一个类 :
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/css/**", "/js/**", "/fonts/**", "/index").permitAll()
.antMatchers("/users/**").hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/login").failureUrl("/login-error");
}
}
重写权限配置方法,静态资源比如css,js,font文件夹下面的不用拦截,而users路径下的需要拦截,表单验证方法。上面是系统的权限管理,然后还有主体的权限管理:
代码语言:javascript复制 /**
* 认证信息管理
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("greenArrow").password("qazwsxedc").roles("ADMIN");
}
}
greenArrow有权限ADMIN,和上面的hasRoles保持一致。