仿牛客项目
- 技术架构与功能模块
- -----------------------------------------------------
- 项目调试技巧(Logger)
- 发送邮件(Email)
- Cookie 和 Session(会话管理)
- Kaptcha生成验证码
- 拦截器(HandlerInterceptor)
- ThreadLocal使多用户登录不冲突
- 过滤敏感词
- AJAX异步请求(页面不刷新)
- 防止脚本注入
- 事务管理(ACID)
- 统一异常处理
- 统一记录日志(AOP)
- 集成Redis
- Redis对登录模块的优化
- Kakfa
- SpringBoot 集成 Kafka
- ElasticSearch + 与SpringBoot整合
- 页面生成CSRF
- 置顶、加精、删除
- Redis高级数据类型
- -----------------------------------------------------
- 功能架构
- ⭐记录一些错误
- 记录一些改错方法
- 后记
技术架构与功能模块
仿牛客项目地址https://gitee.com/Sher-Locked/niuke
技术架构
- SpringBoot
- Spring、SpringMVC、MyBatis、Thymeleaf
- MySQL、Hikari
- Redis、Kafka、ElasticSearch
- SpringSecurity、SpringActuator
功能模块
用户:
- 注册登录
- 个人主页
- 更换头像
- 修改密码
论坛:
- 发布帖子
- 评论回复
- 敏感词过滤
- 点赞功能
- 搜索功能
- 置顶、加精、删除
交友:
- 私信聊天
- 关注、粉丝
- 查看他人主页
-----------------------------------------------------
项目调试技巧(Logger)
断点调试和日志功能。
日志:
//properties
logging.level.com.example.niuke = debug
logging.file.name=d:/log/niuke/xxx.log //存放日志路径
//java
private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
logger.debug("debug");
logger.info("info");
logger.warn("warn");
logger.error("error");
发送邮件(Email)
- 启用邮箱SMTP服务(略)
- 加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
- 配置properties
spring.mail.host=smtp.qq.com
spring.mail.port=465
spring.mail.username=xxxx@qq.com
spring.mail.password=xx.xx.xx
spring.mail.protocol=smtp
spring.mail.properties.mail.smtp.ssl.enable = true
- email工具类
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper messageHelper = new MimeMessageHelper(message);
messageHelper.setFrom(from);
messageHelper.setTo(to);
messageHelper.setSubject(subject);
messageHelper.setText(content, true);
mailSender.send(messageHelper.getMimeMessage());
- 结合Thymeleaf发送html
Context content = new Context();//Thymeleaf的
content.setVariable("username", "愿你被世界温柔以待!");
String process = templateEngine.process("/email/demo", content);
mailClient.sendMail(to, subject, process);
Cookie 和 Session(会话管理)
Cookie
可以通过@CookieValue("code") String code
获取对应的cookie值
//创建cookie
public void setCookie(HttpServletResponse res) {
Cookie cookie = new Cookie("code", CommunityUtil.generateUUID());
cookie.setPath("/test");//cookie作用范围
cookie.setMaxAge(60*10);//cookie存活时间
res.addCookie(cookie);//添加cookie
return "set Cookie";
}
Session
通过session.getAttribute(" ");取值
public String test(HttpSession session){
session.setAttribute("id", 1);
session.setAttribute("naem","dongfang");
return "set session";
}
分布式不用Session的原因:
多服务器时可能在服务端有不同的session,产生session共享问题
解决方案:Session一致性(我的另一篇博文)
Kaptcha生成验证码
- 导包
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
- 配置
@Bean
public Producer kaptchaProducer() {
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width", "100");
properties.setProperty("kaptcha.iamge.height", "40");
properties.setProperty("kaptcha.textproducer.font.size", "32");
properties.setProperty("kaptcha.textproducer.font.color", "0,0,0");
properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQ");
properties.setProperty("kaptcha.textproducer.char.length", "4");
properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");
DefaultKaptcha kaptcha = new DefaultKaptcha();
Config config = new Config(properties);
kaptcha.setConfig(config);
return kaptcha;
}
- 使用
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
//验证码存入session
session.setAttribute("kaptcha",text);
//给浏览器声明返回的类型
response.setContentType("image/png");
try {
OutputStream os = response.getOutputStream();
ImageIO.write(image, "png", os);
} catch (IOException e) {
e.printStackTrace();
}
拦截器(HandlerInterceptor)
拦截器的应用:
- 请求时开始查询登录用户
- 本次请求中持有用户数据
- 模板视图上显示用户数据
- 请求结束时清理用户数据
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}
ThreadLocal使多用户登录不冲突
@Component
public class HostHolder {
private ThreadLocal<User> users = new ThreadLocal<>();
public void setUser(User user) {
users.set(user);
}
public User getUser() {
return users.get();
}
public void clear() {
users.remove();
}
}
过滤敏感词
前缀树:
- 名称:Trie、字典树、查找树
- 特点:查找效率高,消耗内存大
- 应用:字符串检索、词频统计、字符串排序
敏感词过滤器:
- 定义前缀树
- 根据敏感词,初始化前缀树
- 编写过滤敏感词的方法
AJAX异步请求(页面不刷新)
οnclick="send();"
function send() {
$.post(
"/xxx/xxx",
{"name":"xxx","age":"xxx"},
function(data) {
console.log(data);
}
)
}
防止脚本注入
discussPost.setTitle(HtmlUtils.htmlEscape(discussPost.getTitle()));//防止注入
discussPost.setContent(HtmlUtils.htmlEscape(discussPost.getContent()));//防止注入
事务管理(ACID)
ACID
- 悲观锁:1.共享锁 2.排他锁
- 乐观锁
Spring事务管理
-
声明式事务
@Transaction(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
-
编程式事务
@Autowired
private TransactionTemplate transactionTemplate;
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITED);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
return transactionTemplate.execute(new TransactionCallback<Object>(){ 重写方法 });
统一异常处理
@ControllerAdvice 修饰类
@ExceptionHandler 修饰方法
@ModelAttribute 修饰方法
@DataBinder 修饰方法
统一记录日志(AOP)
AOP的实现:
- AspectJ(编译时增强)(字节码)
- SpringAOP(运行时增强)(代理)
SpringAOP:
- JDK动态代理
- CGLib动态代理
集成Redis
Redis编程式事务
@Test
public void testTransactional() {
Object obj = redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
String redisKey = "test";
//启用事务
redisOperations.multi();
redisOperations.opsForSet().add(redisKey, "dongfang");
redisOperations.opsForSet().add(redisKey, "wangquan");
redisOperations.opsForSet().add(redisKey, "tushan");
return redisOperations.exec();
}
});
}
Redis对登录模块的优化
- 使用Redis存储验证码
验证码需要频繁的访问与刷新,对性能要求较高
验证码不需永久保存,通常在很短的时间后就会失效
分布式部署时,存在Session共享的问题 - 使用Redis存储登录凭证
处理每次请求时,都要查询用户的登录凭证,访问的频率非常高 - 使用Redis缓存用户信息
处理每次请求时,都要根据凭证查询用户信息,访问的频率非常高
Kakfa
Kafka 是一个分布式流媒体平台。
应用:消息系统、日志收集、用户行为追踪、流式处理。
特点:高吞吐量、消息持久化、高可靠性、高扩展性。
zookeeper-server-start.bat config\zookeeper.properties
开启zookeeper
kafka-server-start.bat config\server.properties
开启kafka
kafka-topics.bat --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic test
新建类别
kafka-console-producer.bat --broker-list localhost:9092 --topic test
开启生产者
kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic test --from-beginning
开启消费者
阻塞队列
-
BlockingQueue
解决线程通信的问题
阻塞方法:put、take -
生产者消费者模式
生成者:产生数据的线程
消费者:使用数据的线程 -
实现类
ArrayBlockingQueue
LinkedBlockingQueue
PriorityBlockingQueue
SpringBoot 集成 Kafka
- 导包
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
zookeeper-server-start.bat config\zookeeper.properties
开启zookeeper
kafka-server-start.bat config\server.properties
开启kafka- 配置Properties
#kafka
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=niuke-consumer-group
spring.kafka.consumer.enable-auto-commit=true
#ms
spring.kafka.consumer.auto-commit-interval=3000
- springboot调用
@SpringBootTest
public class KafkaTest {
@Resource
private KafkaProducer kafkaProducer;
@Test
public void testKafka() {
kafkaProducer.sendMessage("test", "你好");
kafkaProducer.sendMessage("test", "在吗");
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Component
class KafkaProducer{
@Resource
private KafkaTemplate kafkaTemplate;
public void sendMessage(String topic, String content) {
kafkaTemplate.send(topic, content);
}
}
@Component
class KafkaConsumer {
@KafkaListener(topics = {"test"})
public void handleMessage(ConsumerRecord record) {
System.out.println(record.value());
}
}
ElasticSearch + 与SpringBoot整合
分布式、RestFul 风格的搜索引擎,各种类型的数据的检索,实时搜索服务,PB级数据
术语:
索引(数据库)、类型(表)、文档(行)(Json)、字段(列)。
集群、节点、分片(shards)、副本(replicas)(备份)。
配置:
- 修改elasticsearch.yml
- 配置bin环境变量
- 下载ik分词器(版本对应),解压在
/plugins/ik
下
curl -X GET "localhost:9200/_cat/health?v"
查看健康状况
curl -X GET "localhost:9200/_cat/nodes?v"
查看连接状况
curl -X GET "localhost:9200/_cat/indices?v"
查看索引
curl -X PUT "localhost:9200/test"
新建索引
curl -X DELETE "localhost:9200/test"
删除索引
- 导包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
- 配置properties
# ElasticSearch
spring.elasticsearch.rest.username=niuke
# 9200http 9300tcp
spring.elasticsearch.rest.uris=http://localhost:9200
- 配置entity
@Document(indexName = "discusspost", indexStoreType = "_doc", shards = 6, replicas = 3)
public class DiscussPost {
@Id
private int id;
@Field(type = FieldType.Integer)
private int userId;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String title;
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String content;
@Field(type = FieldType.Integer)
private int type;//0-普通,1-置顶
@Field(type = FieldType.Integer)
private int status;//0-正常,1-精华,2-拉黑
@Field(type = FieldType.Date)
private Date createTime;
@Field(type = FieldType.Integer)
private int commentCount;
@Field(type = FieldType.Double)
private double score;
- 配置dao
@Repository
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost, Integer> {
}
- 使用
NativeSearchQuery build = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "content"))
.withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))
.withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))
.withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))
.withPageable(PageRequest.of(0, 10))
.withHighlightFields(
new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
).build();
页面生成CSRF
<meta name="_csrf" th:content="${_csrf.token}">
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function (e, xhr, options){
xhr.setRequestHeader(header, token)
});
置顶、加精、删除
- 导包
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
- 使用
<html xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
对字段 sec:authorize="hasAnyAuthority('moderator')
一些JS代码
$("#wonderfulButton").attr("disabled","disabled"); 设置其不可用
location.href = "/index"; 跳转页面
Redis高级数据类型
HyperLogLog(合并和去重)
- 采用一种基数算法,用于完成独立总数的统计
- 占据空间小,无论多少数据,只占12K内存
- 不精确,标准误差:0.81%
Bitmap(对位的运算)
- 不是一种独立的数据结构,实际上就是字符串
- 支持按位存取数据,可以将其看成byte数组
- 适合存储索引大量的连续的数据的布尔值
-----------------------------------------------------
功能架构
用户:
- 注册:MD5加密密码,邮箱激活账号
- 登录:Session + Kaptcha + Cookie +ThreadLocal(重构后:Cookie + Redis + Cookie | ThreadLocal + Redis
- 个人主页
- 更换头像:图像URL与本地路径关联 JavaIO流 (重构后:云存储
- 登录状态(防止未登录进入setting等页面)(使用:自定义注解+拦截器
- 修改密码
论坛:
- 发布帖子:AJAX + FastJSON实现异步
- 评论回复
- 敏感词过滤:Trie树
- 点赞功能:Redis实现(set)
交友:
- 私信聊天:MySQL存储
- 关注、粉丝:Redis实现(zset)
- 查看他人主页
⭐记录一些错误
-
当把一些用户删除的时候,他们的推文还在,导致展示推文的时候会查不到这个用户以及推文。
报错信息:
There was an unexpected error (type=Internal Server Error, status=500). An error happened during template parsing (template: "class path resource [templates//index.html]") org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "class path resource [templates//index.html]")
-
同样:我将149号用户删除了呜呜呜呜,找不到用户的名字,一直报错
记录一些改错方法
- 查看报错信息,定位报错位置
- 根据报错信息,找出报错原因
- 根据报错原因, 纠正相关代码
- 找不到原因时:
4.1 断点调试
4.2 logger打印日志信息
技巧:输出错误相关关键语句,细心查看日志
后记
@Autowired和@Resource的区别
Hikari和Druid的区别
Spring
Impl实现同一个接口时,实现IOC时对于容器难以判断 1.加@Primary
注解2.@Repository
时加name,注入时加@Qualifier
HTTP:request.getMethod .getServletPath .getHeadernames 对应发送HTTP的头文本
HTTP:response.setContentType(“text/html;charset=utf-8”) 对应返回HTTP的头文本
@RequestParam
的一些参数 name=" " 对应前端name,required=" " ,defaultValue=" " 默认值