欢迎与我交流QQ:594983498
以下为个人知识点补充,不代表课程内容
使用 th:utext 可以识别转义字符
使用 ||
包裹,可以识别常量
th:class="|page-item ${page.current==1 ? 'disabled' : ''}|"
此标签 class 中会一直带有 page-item 属性
封装 Page 分页对象
- current:当前页码
- limit:每页显示条数
- rows:数据总数,用于计算总页数
- path:查询路径,用于复用分页路径
- offset:数据起始行,current * limit - limit
- total:总页数,rows / limit [+ 1]
- from:起始页码,用于前端显示
- to:终止页码
Page.java
public class Page {
/**
* 当前页码
*/
private Integer current = 1;
/**
* 页记录上限
*/
private Integer limit = 10;
/**
* 数据总数(用于计算总页数)
*/
private Integer rows;
/**
* 查询路径:用于复用分页路径
*/
private String path;
// getter 方法略
public void setCurrent(Integer current) {
if (current >= 1) {
this.current = current;
}
}
public void setLimit(Integer limit) {
if (limit >= 1 && limit <= 100) {
this.limit = limit;
}
}
public void setRows(Integer rows) {
if (rows >= 0) {
this.rows = rows;
}
}
public Integer getOffset() {
return (current - 1) * limit;
}
public Integer getTotal() {
if (rows % limit == 0) {
return rows / limit;
} else {
return rows / limit + 1;
}
}
public Integer getFrom() {
int from = current - 2;
return Math.max(from, 1);
}
public Integer getTo() {
int to = current + 2;
int total = getTotal();
return Math.min(to, total);
}
}
配合 thymeleaf 使用
<!-- 分页 -->
<nav class="mt-5" th:if="${page.rows>0}">
<ul class="pagination justify-content-center">
<li class="page-item">
<!-- /index?current=1-->
<a class="page-link" th:href="@{${page.path}(current=1)}">首页</a>
</li>
<li th:class="|page-item ${page.current==1?'disabled':''}|">
<a class="page-link" th:href="@{${page.path}(current=${page.current-1})}">上一页</a>
</li>
<li th:class="|page-item ${page.current==i?'active':''}|" th:each="i:${#numbers.sequence(page.from,page.to)}">
<a class="page-link" th:href="@{${page.path}(current=${i})}" th:text="${i}">1</a>
</li>
<li th:class="|page-item ${page.current==page.total?'disabled':''}|">
<a class="page-link" th:href="@{${page.path}(current=${page.current+1})}">下一页</a>
</li>
<li class="page-item">
<a class="page-link" th:href="@{${page.path}(current=${page.total})}">末页</a>
</li>
</ul>
</nav>
注意:要在邮箱设置中开启 smtp 配置
springboot 依赖包 pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
配置文件 application.yaml
:
spring:
mail:
host: smtp.qq.com
port: 465
username: [email protected]
password: xxxxxxxxxxx
protocol: smtps
properties:
mail:
smtp:
auth: true
ssl:
enable: true
邮件发送工具类 MailClient.java
:
@Component
public class MailClient {
private static final Logger logger = LoggerFactory.getLogger(MailClient.class);
@Resource
private JavaMailSender mailSender;
@Value("${spring.mail.username}")
private String from;
public void sendMail(String to, String subject, String content) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
mailSender.send(helper.getMimeMessage());
} catch (MessagingException e) {
logger.error("发送邮件失败:" + e.getMessage());
}
}
}
@Resource
private MailClient mailClient;
@Resource
private TemplateEngine templateEngine;
// 简单邮件发送
@Test
public void testTextMail() {
mailClient.sendMail("[email protected]", "TEST", "Welcome.");
}
// 发送 HTML 模板内容
@Test
public void testHtmlMail() {
Context context = new Context();
context.setVariable("username", "sunday");
String content = templateEngine.process("/mail/activation", context);
System.out.println(content);
mailClient.sendMail("[email protected]", "HTML", content);
}
这里使用的日志框架是 log4j2
log4j2.yml
Configuation:
status: warn # 打印日志级别
monitorInterval: 30
Appenders:
# 输出到控制台
Console:
name: CONSOLE # 命名
target: SYSTEM_OUT
PatternLayout:
pattern: "%d{yyyy-MM-dd HH:mm:ss,SSS}:%4p %t (%F:%L) - %m%n"
RollingFile: #输出到文件
- name: ROOLLING_FILE
fileName: "work03-web/logs/batch.log"
filePattern: "work03-web/logs/$${date:yyyy-MM}/batch -%d{yyyy-MM-dd}-%i.log.gz"
PatternLayout:
pattern: "%d{yyyy-MM-dd HH:mm:ss,SSS}:%4p %t (%F:%L) - %m%n"
Policies:
TimeBasedTriggeringPolicy: # 按天分类
modulate: true
interval: 1 # 多久滚动一次 hour
DefaultRolloverStrategy: #文件最多存多少
max: 5
Loggers:
Root:
level: info
AppenderRef:
- ref: CONSOLE
- ref: ROOLLING_FILE
application.yml
logging:
config: classpath:log4j2.yml
例:
@PostMapping("/mvc")
public String mvc(Model model, User user, String code) {
// some code.
return "mvc"
}
对于 Controller 中的方法中,如果参数中包含实体对象,在方法调用前,会自动实例化 Model 和实体对象,并把实体对象保存到 model 对象中,所以在模板引擎中可以直接取到实体对象的值。
如果参数是普通参数类型,而想要取参数中传递的值,可以使用 request
域对象取值。
当多次请求处理的逻辑都相同时,考虑使用拦截器统一处理,而不是每次都进行相同的处理请求。
- 拦截器组件
@Component
public class AlphaInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class);
// 在Controller之前执行, handler 指的是拦截的目标 controller 请求方法
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.debug("preHandle: " + handler.toString());
return true;
}
// 在Controller之后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.debug("postHandle: " + handler.toString());
}
// 在TemplateEngine之后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
logger.debug("afterCompletion: " + handler.toString());
}
}
- 配置拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AlphaInterceptor alphaInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(alphaInterceptor)
// 配置过滤请求
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
// 配置要拦截的请求
.addPathPatterns("/register", "/login");
}
自定义文件上传位置:
# application.yml
community:
path:
upload: E:\Documents\Projects\Community\upload
...
传输文件给页面
// UserController
@GetMapping("/header/{fileName}")
public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response) {
// 文件存储路径
fileName = uploadPath + "/" + fileName;
// 文件后缀类型
String suffix = fileName.substring(fileName.lastIndexOf("."));
// 响应图片
response.setContentType("image/" + suffix);
try (OutputStream os = response.getOutputStream(); FileInputStream fis = new FileInputStream(fileName)) {
byte[] buffer = new byte[1024];
int b = 0;
while ((b = fis.read(buffer)) != -1) {
os.write(buffer, 0, b);
}
} catch (IOException e) {
logger.error("读取头像文件失败: " + e.getMessage());
}
}
这里用于检查登录状态,拦截非法请求(未登录的请求)
-
元注解
@Target
:注解可以作用的位置(类/方法/属性)@Retention
:注解有效时间(编译时有效/运行时有效)@Document
:是否生成文档@Inherited
:用于决定是否继承(子类是否继承父类标注的注解) -
如何使用注解(反射)
Method.getDeclaredAnnotations()
:获取这个方法上所有的注解Method.getAnnotation(Class<T> annotationClass)
:获取该类型的注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {
@Resource
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
if (loginRequired != null && hostHolder.getUser() == null) {
response.sendRedirect(request.getContextPath() + "/login");
return false;
}
}
return true;
}
}
- 前缀树
- 名称:Trie、字典树、查找树
- 特点:查找效率高、消耗内存大
- 应用:字符串检索、词频统计、字符串排序
- 敏感词过滤器
- 定义前缀树
- 根据敏感词,初始化前缀树
- 编写过滤敏感词的方法
完整代码
@Component
public class SensitiveFilter {
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
/**
* 敏感词替换符
*/
private static final String REPLACEMENT = "***";
/**
* 根节点
*/
private TrieNode rootNode = new TrieNode();
/**
* 初始化前缀树
* @PostConstruct:标注一个初始化方法,在容器初始化 bean 时,该初始化方法自动执行
*/
@PostConstruct
public void init() {
// 按行读取保存敏感词数据的文件
try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt"); BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
String keyword;
while ((keyword = reader.readLine()) != null) {
// 添加到前缀树
this.addKeyword(keyword);
}
} catch (IOException e) {
logger.error("加载敏感词文件失败: {}", e.getMessage());
e.printStackTrace();
}
}
/**
* 将一个敏感词添加到前缀树中去
*/
private void addKeyword(String keyword) {
TrieNode tempNode = rootNode;
for (int i = 0; i < keyword.length(); i++) {
char c = keyword.charAt(i);
TrieNode subNode = tempNode.getSubNode(c);
if (subNode == null) {
// 初始化子节点
subNode = new TrieNode();
tempNode.setSubNode(c, subNode);
}
// 指向子节点,进入下一轮循环
tempNode = subNode;
// 设置结束标识
if (i == keyword.length() - 1) {
tempNode.isKeywordEnd = true;
}
}
}
/**
* 过滤敏感词算法
* @param text 待过滤文本
* @return 过滤后的文本
*/
public String filter(String text) {
if (StringUtils.isBlank(text)) {
return null;
}
// 指针1 默认指向前缀树根节点
TrieNode tempNode = rootNode;
// 指针2 默认指向文本字符串首位 只向后移动,到敏感词开始时停下
int begin = 0;
// 指针3 默认指向文本字符串首位,跟着指针2, 当指针2遇到敏感词停下时,指针3继续移动判断是否是敏感词
int position = 0;
// 结果文本
StringBuilder sb = new StringBuilder();
while (position < text.length()) {
char c = text.charAt(position);
// 跳过符号
if (isSymbol(c)) {
// 若指针1 处于根节点,将此符号计入结果不跳过,让指针2向下走一步
if (tempNode == rootNode) {
sb.append(c);
begin++;
}
// 无论符号在开头或中间,指针3都向下走一步
position++;
continue;
}
// 检查下级节点
tempNode = tempNode.getSubNode(c);
if (tempNode == null) {
// 以begin为开头的字符串不是敏感词
sb.append(text.charAt(begin));
// 进入下一个位置
position = ++begin;
// 重新指向根节点
tempNode = rootNode;
} else if (tempNode.isKeywordEnd) {
// 发现了敏感词,将begin到position字符串替换掉
sb.append(REPLACEMENT);
// 进入下一个位置
begin = ++position;
// 重新指向根节点
tempNode = rootNode;
} else {
// 继续检查下一个字符
position++;
}
}
// 将最后一批字符计入结果
sb.append(text.substring(begin));
return sb.toString();
}
/**
* 判断字符是否是符号
* 0x2E80 ~ 0x9FFF 东亚文字范围
* @param c
* @return
*/
private boolean isSymbol(Character c) {
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
/**
* 定义前缀树 数据结构
*/
private class TrieNode {
// 关键词结束标识
private boolean isKeywordEnd = false;
// 子节点(key 是下级字符,value 是下级节点)
private Map<Character, TrieNode> subNodes = new HashMap<>();
// 添加子节点
public void setSubNode(Character c, TrieNode node) {
subNodes.put(c, node);
}
// 获取子节点
public TrieNode getSubNode(Character c) {
return subNodes.get(c);
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
}
}
- isolation: 隔离级别
- propagation: 传播机制
// REQUIRED: 支持当前事务(外部事务),如果不存在则创建新事务.
// REQUIRES_NEW: 创建一个新事务,并且暂停当前事务(外部事务).
// NESTED: 如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),否则就会REQUIRED一样.
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public Object save1() {
// 新增用户
User user = new User();
user.setUsername("alpha");
user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("[email protected]");
user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("Hello");
post.setContent("新人报道!");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
Integer.valueOf("abc");
return "ok";
}
- @ControllerAdvice
- 用于修饰类,表示该类是Controller的全局配置类。
- 在此类中,可以对Controller进行如下三种全局配置: 异常处理方案、绑定数据方案、绑定参数方案。
- @ExceptionHandler
- 用于修饰方法,该方法会在Controller出现异常后被调用,用于处理捕获到的异常。
- @ModelAttribute
- 用于修饰方法,该方法会在Controller方法执行前被调用,用于为Model对象绑定参数。
- @DataBinder
- 用于修饰方法,该方法会在Controller方法执行前被调用,用于绑定参数的转换器。
@ControllerAdvice(annotations = Controller.class) //只对标注Controller注解的类进行处理
public class ExceptionAdvice {
private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);
@ExceptionHandler({Exception.class}) //注解参数是拦截的异常类型
public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
logger.error("服务器发生异常: {}", e.getMessage());
for (StackTraceElement element : e.getStackTrace()) {
logger.error(element.toString());
}
String header = request.getHeader("x-requested-with");
// 当前请求为异步请求
if ("XMLHttpRequest".equals(header)) {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(1, "服务器异常!"));
} else {
// 请求为网页请求
response.sendRedirect(CommunityUtil.contextPathJudge(request.getContextPath()) + "/error");
}
}
}