设计思路
总体设计
自定义框架dataflow
上学期学了软件工程。提了一下面向数据流的设计。我就想写一个小框架。把设计好的数据流,可以无缝地转换为java代码。并且高可扩展,天然地支持逐步细化。本来受到AOP的影响。节点我都想加个切点作用的节点。结果我发现这样写很啰嗦,无穷无尽的感觉。最后我放弃了这种方案,改用子图的方式。我觉的这可以最大程度上满足需求的变化。一般来说最高层的图最抽象。需求变化基本不会影响它。最底层的function就像组成这世界的基本元素也不怎么会变。变化的最多的是中间层,也是最一团乱麻的,如果有个类管理代码块之间的联系那岂不是很好。
我写了一个模仿数据流的一个抽象对像。他有映射,链接和管道等方法。用链接把一个个函数式接口串起来,形成一个图。当需要扩展时调用原来的节点的map方法映射成一个子图就行。这样就天然支持逐步细化和迭代。或者是加个链接。如果是用ioc容器来管理和注入就更方便了。但限于时间。我还没写基于spring版本的dataFlow.
复用的
我们老师一直给我们灌输对扩展开放对修改关闭的思想。但有些时候修改代码又是必要的。以前我我们总说代码复用。为啥不数据复用呢?数据从刚开始一个抽象的语义到一个具体的语义其中可以复用的地方太多了。
关于定位错误的思考
另外关于从数据流的角度定位错误。暂不考虑错误复现的问题。我们报错的位置往往和错误的原因所在的位置相去甚远。这样我们要找到错误原因就很难,要是有个管理数据流的类,这种情况可能会大为改善。如果你有一些数据运行正常。一些数据运行异常。想象一下,数据流不断分叉,最终正确和错误数据流分道扬镳的位置极可能是错误的原因所在。准确一点说是该节点以上的节点都有可能。根据这个想法就可一写一个简陋的错误定位器(还没来得及写)。
并发
并发的管理比较简单。这抽象级别的也确实做不了啥。主要想法就是越抽象的模块之间并发程度应该越高。所以我设置了一个并发等级。每进入一个节点就减一,最后就是同步的代码。并发节点之间的数据是独立的,不需要🔓。如果你想共享一些数据可以把等级设为-1,自己管理并发。对于大量数据推荐用Java8的并发流
具体代码
- 提供了两个工具类可以通过
stream() 和utils() 函数获得实例。 - 一个工具函数
toMermaid 用来可视化,把输出张贴到Typora里。红色表示有子图的节点。 StreamUtils 的pipe 函数可以串联多个Flow 。但不建议。因为违背了我纯函数的设想。还是建议通过加节点和链接串联。
DATAFLOW
package cn.lyf.myblog.dataFlow;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import java.util.concurrent.*;
public class Flow<T> {
final private Map<String, UnaryOperator<T>> nodes = new ConcurrentHashMap<>();
final private Map<String, List<String>> edges = new ConcurrentHashMap<>();
final private Map<String, Flow<T>> map = new ConcurrentHashMap<>();
final private Set<String> jump = new CopyOnWriteArraySet<>();
final private int level;
public static <E> Flow<E> of(Class<E> e) {
return new Flow<E>();
}
public Flow() {
this.level = 0;
}
public Flow(int level) {
this.level = level;
}
public FlowUtils<T> utils() {
return new FlowUtils<>(this);
}
public StreamUtils<T> stream() {
return new StreamUtils<>(this);
}
public Flow<T> setHandler(String k, UnaryOperator<T> fx) {
nodes.put(k, fx);
return this;
}
public Flow<T> removeHandler(String k) {
nodes.remove(k);
return this;
}
public Flow<T> shortcut(String k) {
if (jump.add(k))
return this;
else
return null;
}
public Flow<T> unshortcut(String k) {
if (jump.remove(k))
return this;
else
return null;
}
public void start(String k, T data) {
start(k, data, level);
}
public Flow<T> map(String key, Flow<T> subFlow, UnaryOperator<T> fx) {
map(key, subFlow);
return subFlow.setHandler(key, fx);
}
public Flow<T> map(String key, Flow<T> subFlow) {
map.put(key, subFlow);
UnaryOperator<T> fx = nodes.get(key);
if (fx != null) {
subFlow.setHandler(key, fx);
for (String to : edges.getOrDefault(key, new ArrayList<>())) {
subFlow.link(key, to);
subFlow.map.put(to, this);
}
}
return this;
}
public Flow<T> removemap(String key) {
map.remove(key);
return this;
}
public Flow<T> link(String from, String to) {
List<String> l = edges.get(from);
if (l == null) {
l = new CopyOnWriteArrayList<>();
edges.put(from, l);
}
if (l.add(to))
return this;
return null;
}
public Flow<T> unlink(String from, String to) {
List<String> l = edges.get(from);
if (l != null && l.remove(to))
return this;
return null;
}
private void start(String k, T data, int d) {
if (!jump.contains(k)) {
Flow<T> subFlow = map.get(k);
if (subFlow != null) {
subFlow.start(k, data, d - 1);
return;
}
UnaryOperator<T> fx = nodes.get(k);
if (fx == null)
return;
data = fx.apply(data);
if (data == null)
return;
}
List<String> list = edges.getOrDefault(k, new ArrayList<>());
if (d < 0 || list.size() < 2) {
for (String v : list) {
start(v, data, d);
}
} else {
final T msg = data;
for (String v : list) {
new Thread(new Runnable() {
@Override
public void run() {
start(v, DeepCopy.copy(msg), d - 1);
}
}).start();
}
}
}
public synchronized void toMermaid(String[] starts) {
System.out.println("```mermaid");
System.out.println("graph TD");
System.out.println("classDef black fill:#222,color:#fff,stroke:#000,stroke-width:2px;");
System.out.println("classDef red fill:#f22,color:#fff,stroke:#000,stroke-width:2px;");
for (String s : starts)
toMermaid(null, s);
System.out.println("```");
}
private void toMermaid(String f, String s) {
System.out.println(s + "(('" + s + "'))");
System.out.println("class " + s + " " + (map.get(s) == null ? "black" : "red"));
if (f != null)
System.out.println(f + "-->" + s);
List<String> ns = edges.get(s);
if (ns == null)
return;
for (String next : ns)
toMermaid(s, next);
}
public static class FlowUtils<T> {
private String last;
final private Flow<T> flow;
public FlowUtils<T> print() {
return setHandler(i -> {
System.out.println(i);
return i;
});
}
public FlowUtils(Flow<T> flow) {
this.flow = flow;
}
public FlowUtils<T> start(String k, Consumer<T> fx) {
return start(k, voidable(fx));
}
public FlowUtils<T> start(String k, Supplier<T> fx) {
return start(k, voidable(fx));
}
public FlowUtils<T> start(String k, Runnable fx) {
return start(k, voidable(fx));
}
public FlowUtils<T> start(String k, UnaryOperator<T> fx) {
flow.setHandler(k, fx);
return start(k);
}
public FlowUtils<T> start(String k) {
last = k;
return this;
}
public FlowUtils<T> link(String k) {
flow.link(last, k);
last = k;
return this;
}
public FlowUtils<T> link(String k, Consumer<T> fx) {
return link(k, voidable(fx));
}
public FlowUtils<T> link(String k, Supplier<T> fx) {
return link(k, voidable(fx));
}
public FlowUtils<T> link(String k, Runnable fx) {
return link(k, voidable(fx));
}
public FlowUtils<T> link(String k, UnaryOperator<T> fx) {
flow.setHandler(k, fx);
return link(k);
}
public FlowUtils<T> setHandler(Consumer<T> fx) {
return setHandler(voidable(fx));
}
public FlowUtils<T> setHandler(Supplier<T> fx) {
return setHandler(voidable(fx));
}
public FlowUtils<T> setHandler(Runnable fx) {
return setHandler(voidable(fx));
}
public FlowUtils<T> setHandler(UnaryOperator<T> fx) {
String[] ss = last.split("-");
int n = 0;
if (ss.length == 2)
n = Integer.valueOf(ss[1]) + 1;
String k = ss[0] + "-" + n;
return link(k, fx);
}
public FlowUtils<T> map(Flow<T> f) {
flow.map(last, f);
return this;
}
private UnaryOperator<T> voidable(Consumer<T> fx) {
return new UnaryOperator<T>() {
public T apply(T t) {
fx.accept(t);
return t;
};
};
}
private UnaryOperator<T> voidable(Supplier<T> fx) {
return new UnaryOperator<T>() {
public T apply(T t) {
return fx.get();
};
};
}
private UnaryOperator<T> voidable(Runnable fx) {
return new UnaryOperator<T>() {
public T apply(T t) {
fx.run();
return t;
};
};
}
}
public static class StreamUtils<T> {
private Stream<T> stream;
final private Flow<T> flow;
public StreamUtils(Flow<T> flow) {
this.flow = flow;
}
public StreamUtils<T> batch(Stream<T> dataStream, String k) {
dataStream.forEach(d -> flow.start(k, d));
return this;
}
public StreamUtils<T> pipe(String k) {
return pipe(stream, k);
}
public StreamUtils<T> pipe(Stream<T> dataStream, String k) {
stream = dataStream.map(d -> {
flow.start(k, d, -1);
return d;
});
return this;
}
public List<T> collect() {
return stream.collect(Collectors.toList());
}
}
}
应用
在评论处理的类里我就尝试使用了下。主要是因为,为了满足范式。表有点多,有些嵌套查询。如下在初始化的时候先绑定处理函数
final Flow<ObjectNode> flow = new Flow<>();
@Autowired
CommentsMapper commentsMapper;
@Autowired
UserInfo2Mapper userInfo2Mapper;
public CommentService() {
flow.setHandler("getUserInfo2", this::getUserInfo2);
flow.setHandler("getReplys", this::getReplys);
flow.setHandler("getReplyUser", this::getReplyUser);
}
然后核心代码就一句话。把数据串起来。
Stream<ObjectNode> ss = list.stream().map(e -> mapper.valueToTree(e));
List<ObjectNode> l = flow.stream().pipe(ss, "getUserInfo2").pipe("getReplys").pipe("getReplyUser").collect();
如果要扩展就把其中的节点重新注入就好了。或者分叉一下。 但是由于之前没想做管道方法,导致管道只能把数据整个往下传,没有定位功能导致复用功能大打折扣。 如下 获取评论的时候由于也要调获取用户信息。要复用只能在里面进行调用。这其实就耦合了。
public ObjectNode getReplyUser(ObjectNode json) {
JsonNode reply = json.get("reply");
if (reply == null)
return json;
ObjectNode res = getUserInfo2(reply.deepCopy());
json.set("reply", res);
return json;
}
前后端交互
总的设想就是用json接口来实现数据交互。让前后端分离。thymeleaf 模板主要用来初始化vue 或导入vue 模板。vue 负责后续请求json接口,刷新页面。
日志和错误处理
日志使用springboot 自带的slf4j 错误使用aop和@ControllerAdvic注解
权限管理
权限管理使用shiro框架进行拦截,和授权。
具体设计
数据表设计
基本就是一个主表加一个映射表的样子。
DROP TABLE IF EXISTS `user`;
CREATE TABLE IF NOT EXISTS `user` (
`uid` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`username` VARCHAR(255) NOT NULL UNIQUE,
`password` VARCHAR(255),
PRIMARY KEY (`uid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
DROP TABLE IF EXISTS `user_info`;
CREATE TABLE IF NOT EXISTS `user_info` (
`uid` INT UNSIGNED NOT NULL UNIQUE,
`birth` DATE DEFAULT NULL,
`qq` VARCHAR(255),
`wechat` VARCHAR(255),
`mail` VARCHAR(255),
`tel` VARCHAR(255),
PRIMARY KEY (`uid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
DROP TABLE IF EXISTS `user_info2`;
CREATE TABLE IF NOT EXISTS `user_info2` (
`uid` INT UNSIGNED NOT NULL UNIQUE,
`avatar` VARCHAR(255),
`brief` VARCHAR(255),
PRIMARY KEY (`uid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
DROP TABLE IF EXISTS `role`;
CREATE TABLE IF NOT EXISTS `role` (
`rid` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL UNIQUE,
PRIMARY KEY (`rid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
DROP TABLE IF EXISTS `perm`;
CREATE TABLE IF NOT EXISTS `perm` (
`pid` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL UNIQUE,
PRIMARY KEY (`pid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
DROP TABLE IF EXISTS `user_perm`;
CREATE TABLE IF NOT EXISTS `user_perm` (
`uid` INT UNSIGNED NOT NULL,
`pid` INT UNSIGNED NOT NULL,
`describe` VARCHAR(255) DEFAULT NULL,
PRIMARY KEY (`uid`, `pid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE IF NOT EXISTS `user_role` (
`uid` INT UNSIGNED NOT NULL,
`rid` INT UNSIGNED NOT NULL,
PRIMARY KEY (`uid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
DROP TABLE IF EXISTS `comments`;
CREATE TABLE IF NOT EXISTS `comments` (
`cid` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`bid` INT UNSIGNED NOT NULL,
`uid` INT UNSIGNED NOT NULL,
`pcid` INT UNSIGNED DEFAULT NULL,
`content` VARCHAR(255) NOT NULL,
`date` DATE NOT NULL,
`is_read` boolean,
PRIMARY KEY (`cid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
DROP TABLE IF EXISTS `blog`;
CREATE TABLE IF NOT EXISTS `blog` (
`bid` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`uid` INT UNSIGNED NOT NULL,
`title` VARCHAR(255) NOT NULL,
`url` VARCHAR(255) NOT NULL,
`date` DATE NOT NULL,
`views` INT UNSIGNED DEFAULT 0,
`likes` INT UNSIGNED DEFAULT 0,
`favorites`INT UNSIGNED DEFAULT 0,
`summary` VARCHAR(255),
`attr` TINYINT DEFAULT 0,
`url` VARCHAR(255),
UNIQUE KEY (`uid`, `title`),
PRIMARY KEY (`bid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
DROP TABLE IF EXISTS `likes`;
CREATE TABLE IF NOT EXISTS `likes` (
`lid` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`uid` INT UNSIGNED NOT NULL,
`bid` INT UNSIGNED NOT NULL,
`is_read` boolean DEFAULT false,
UNIQUE KEY (`uid`, `bid`),
PRIMARY KEY (`lid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
DROP TABLE IF EXISTS `favorites`;
CREATE TABLE IF NOT EXISTS `favorites` (
`fid` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`uid` INT UNSIGNED NOT NULL,
`name` VARCHAR(255),
`date` DATE NOT NULL,
`total` INT UNSIGNED DEFAULT 0,
UNIQUE KEY (`uid`, `name`),
PRIMARY KEY (`fid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
DROP TABLE IF EXISTS `favorites_blog`;
CREATE TABLE IF NOT EXISTS `favorites_blog` (
`fid` INT UNSIGNED NOT NULL,
`bid` INT UNSIGNED NOT NULL,
PRIMARY KEY (`fid`, `bid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
DROP TABLE IF EXISTS `label`;
CREATE TABLE IF NOT EXISTS `label` (
`lid` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255),
`date` DATE NOT NULL,
UNIQUE KEY (`name`),
PRIMARY KEY (`lid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
DROP TABLE IF EXISTS `label_blog`;
CREATE TABLE IF NOT EXISTS `label_blog` (
`lid` INT UNSIGNED NOT NULL,
`bid` INT UNSIGNED NOT NULL,
`uid` INT UNSIGNED NOT NULL,
PRIMARY KEY (`lid`, `bid`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
评论设计
template
<template id="r">
<div class="comment">
<a v-if="anchor" class="avatar" :id="c.cid">
<img :src="`/res/image/${c.user.avatar}`">
</a>
<a v-else class="avatar" :href="`#${c.cid}`">
<img :src="`/res/image/${c.user.avatar}`">
</a>
<div class="content">
<a class="author">
{{c.user.username}}
</a>
<a class="author" v-if="c.reply !== undefined">
对 {{c.reply.user.username}} 说
</a>
<div class="metadata">
<span class="date">
{{c.date}}
</span>
</div>
<div class="text">{{c.content}}</div>
<div class="actions"><a class="reply">回复</a></div>
</div>
<div class="comments" v-if="c.reply !== undefined">
<comment-info :c="c.reply" :anchor="false"></comment-info>
</div>
</div>
</template>
vue
var ajax = {
data() {
return {
comments: [],
blog: {}
}
},
mounted() {
getJson("/blog/[[${bid}]]", (a) => { this.blog = a }).then(() => {
var url = `/toHtml/${this.blog.url}`;
axios.get(url).then(response => {
this.blog.content = response.data;
})
});
postJson("/comments/blog", {
"blogId": "[[${bid}]]",
"pageNo": 0,
"pageSize": 20
}, (a) => { this.comments = a.data });
}
}
var app = Vue.createApp(ajax)
app.component('comment-info', {
props: ['c', 'anchor'],
template: '#r'
})
app.mount('#app')
调用
<comment-info v-for="comment in comments" :c="comment" :anchor="true"></comment-info>
刚开始是想做个多级评论的,但后来想要是评论多了那岂不是要到评论外面去了?果断否定。而且两级评论不仅有利于数据库存储,也有利于查询,防止mysql嵌套查询。另一方面还要实现跳转功能,可以从回复的评论跳到被回复的评论,这里用html锚点链接外加一个anchor变量判断是否是回复。当为回复时设为跳转链接,否者设为
博客设计
博客为了方便编写集成了makdown编辑器。这里用的是别人的库
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>0.17.0</version>
</dependency>
效果 通过调用/blog/[[${bid}]] 接口获取博客基本信息,再调用/toHtml/${this.blog.url} 接口获取渲染后的博客内容。其中getJson 是自己封装的axios方法。 getJSon
<th:block th:fragment="vue">
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.1.5/vue.global.prod.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script>
function postJson(url, jsons, callback) {
return axios.post(url, jsons, {
header: {
'Content-Type': 'application/json'
}
}).then(response => {
var json = response.data;
if (json.code == 200) {
callback(json.result);
} else {
alert(json.message);
}
}).catch(function (error) {
console.log(error);
});
}
function getJson(url, callback) {
return axios.get(url).then(response => {
var json = response.data;
if (json.code == 200) {
callback(json.result);
} else {
alert(json.message);
}
}).catch(function (error) {
console.log(error);
});
}
</script>
</th:block>
getJson("/blog/[[${bid}]]", (a) => { this.blog = a }).then(() => {
var url = `/toHtml/${this.blog.url}`;
axios.get(url).then(response => {
this.blog.content = response.data;
})
});
展示
抽象了几个展示的模板,最开始是用tympleaf 的 fragment 后来写到一半发现接口都是json接口被迫改用vue 。
<template id="ac" th:fragment="ac">
<div class="ui segment grid stackable">
<div class="ui five wide column">
<a :href="`/article/${article.bid}`">
<img :src="`${root}res/image/${article.img}`" class="ui rounded image" alt="预览图"
style="height: 9em;">
</a>
</div>
<div class="ui eleven wide column">
<h3 class="ui header">{{article.title}}</h3>
<p>{{article.summary}}</p>
<div class="ui grid">
<div class="eleven wide column">
<div class="ui horizontal link list">
<div class="item">
<img :src="`${root}res/image/${article.author.avatar}`" alt="" class="ui avatar image">
<div class="content"><a href="#" class="header">{{article.author.username}}</a></div>
</div>
<div class="item">
<i class="calendar icon"></i>{{article.date}}
</div>
<div class="item">
<i class="eye icon"></i>{{article.views}}
</div>
</div>
</div>
<div class="five wide column">
<a :href="`/article/${article.bid}`" class="ui label teal">阅读原文</a>
</div>
</div>
</div>
</div>
</template>
这里查询的数据比较复杂,我用了java8的流来处理 获取最近博客列表 不过后来想想,换成vue多次请求不是更香吗?不过为了和接口文档一致只能这样了。
public ObjectNode getBlogLately(ObjectNode json) throws Exception {
Integer uid = getInteger(json, "/uid");
Integer pageNo = getInteger(json, "/pageNo");
Integer limit = getInteger(json, "/pageSize");
ObjectNode res = mapper.createObjectNode();
res.put("success", true);
res.put("code", 200);
res.put("message", "博客列表获取成功!");
List<Blog> blogs = blogMapper.selectBlogLately(uid, limit, pageNo * limit);
res.putPOJO("result", blogs.stream().map(this::getMoreUserInfo).collect(Collectors.toList()));
return res;
}
管理
发布
管理
权限
@Configuration
public class ShiroConfig {
@Bean("UserRealm")
public UserRealm getRealm() {
return new UserRealm();
}
@Bean("mySecurityManager")
public DefaultWebSecurityManager getMySecurityManager(@Qualifier("UserRealm") UserRealm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean getMyFactoryBean(
@Qualifier("mySecurityManager") DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFactoryBean = new ShiroFilterFactoryBean();
shiroFactoryBean.setSecurityManager(securityManager);
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/*", "user");
filterMap.put("/content/*", "authc");
filterMap.put("/res/**","anon");
shiroFactoryBean.setLoginUrl("/toLogin");
shiroFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFactoryBean;
}
邮件
邮件工具类 邮件使用springboot的spring-boot-starter-mail 用自己的qq邮箱作代理服务器,发送验证码。并用map存储。同时设置计时线程池,到时间自动移除验证码。
scheduledExecutorService.schedule(new Thread(() -> {
ele e = map.get(email);
if (e != null && e.date.equals(date)) {
map.remove(email);
}
}), 5 * 60, TimeUnit.SECONDS);
@PostMapping("/sendCode")
public JsonNode sendCode(@RequestBody JsonNode json) throws Exception {
ObjectNode res = mapper.createObjectNode();
try {
emailuUtils.sendEmail(getString(json, "/email"));
res.put("success", true);
res.put("code", 200);
res.put("message", "发送验证码成功!");
} catch (MailException e1) {
res.put("success", false);
res.put("code", 471);
res.put("message", e1.getMessage());
} catch (MessagingException e2) {
res.put("success", false);
res.put("code", 472);
res.put("message", e2.getMessage());
}
return res;
}
@PostMapping("/checkCode")
public JsonNode verCode(@RequestBody JsonNode json) throws Exception {
boolean b = emailuUtils.verification(getString(json, "/email"), getString(json, "/vercode"));
ObjectNode res = mapper.createObjectNode();
if (b) {
res.put("success", true);
res.put("code", 200);
res.put("message", "验证成功!");
return res;
}
res.put("success", false);
res.put("code", 477);
res.put("message", "验证失败!");
return res;
}
package cn.lyf.myblog.controller.api;
import java.util.concurrent.*;
import java.text.SimpleDateFormat;
import java.util.*;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
@Component
public class EmailUtils {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
Map<String, ele> map = new ConcurrentHashMap<>();
@Autowired
JavaMailSenderImpl mailSender;
String name="MyBlog";
String from="MyBlog验证中心";
class ele {
String code;
Date date;
public ele(String code, Date date) {
this.code = code;
this.date = date;
}
}
public void sendEmail(String email) throws MailException,MessagingException {
int code = (int) ((Math.random() * 9 + 1) * 1000);
String verCode = String.valueOf(code);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
final Date date = new Date();
String time = simpleDateFormat.format(date);
map.put(email, new ele(verCode, date));
scheduledExecutorService.schedule(new Thread(() -> {
ele e = map.get(email);
if (e != null && e.date.equals(date)) {
map.remove(email);
}
}), 5 * 60, TimeUnit.SECONDS);
MimeMessage mimeMessage = null;
MimeMessageHelper helper = null;
mimeMessage = mailSender.createMimeMessage();
helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("【"+name+"】 注册账号验证码");
helper.setText("<h3>\n" + "\t<span style=\"font-size:16px;\">亲爱的用户:</span> \n" + "</h3>\n" + "<p>\n"
+ "\t<span style=\"font-size:14px;\"> </span><span style=\"font-size:14px;\"> <span style=\"font-size:16px;\"> 您好!您正在进行邮箱验证,本次请求的验证码为:<span style=\"font-size:24px;color:#FFE500;\"> "
+ verCode
+ "</span>,本验证码5分钟内有效,请在5分钟内完成验证。(请勿泄露此验证码)如非本人操作,请忽略该邮件。(这是一封自动发送的邮件,请不要直接回复)</span></span>\n"
+ "</p>\n" + "<p style=\"text-align:right;\">\n"
+ "\t<span style=\"background-color:#FFFFFF;font-size:16px;color:#000000;\"><span style=\"color:#000000;font-size:16px;background-color:#FFFFFF;\"><span class=\"token string\" style=\"font-family:"font-size:16px;color:#000000;line-height:normal !important;background-color:#FFFFFF;\">"+name+"</span></span></span> \n"
+ "</p>\n" + "<p style=\"text-align:right;\">\n"
+ "\t<span style=\"background-color:#FFFFFF;font-size:14px;\"><span style=\"color:#FF9900;font-size:18px;\"><span class=\"token string\" style=\"font-family:"font-size:16px;color:#000000;line-height:normal !important;\"><span style=\"font-size:16px;color:#000000;background-color:#FFFFFF;\">"
+ time
+ "</span><span style=\"font-size:18px;color:#000000;background-color:#FFFFFF;\"></span></span></span></span> \n"
+ "</p>", true);
helper.setTo(email);
helper.setFrom("xxxxx@qq.com");
mailSender.send(mimeMessage);
}
boolean verification(String email, String code) {
ele e = map.get(email);
if (e != null && e.code.equals(code)) {
return true;
}
return false;
}
}
日志及错误处理
日志主要用个aop切面实现
@Aspect
@Component
public class LogAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Pointcut("execution(* cn.lyf.myblog.controller.*.*(..))")
public void log() {
}
@Before("log()")
public void doBeafore(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
System.out.println("url:" + request.getRequestURI());
System.out.println("ip:" + request.getRemoteAddr());
System.out.println(joinPoint.getSignature().getDeclaringTypeName());
System.out.println(joinPoint.getSignature().getName());
for (Object o : joinPoint.getArgs()) {
System.out.println(o);
}
}
@AfterReturning(returning = "result", pointcut = "log()")
public void doAfterReturn(Object result) {
logger.info("\nResult:{}", result);
}
}
错误处理,利用ControllerAdvice注解把错误url和错误信息渲染到页面上
@ControllerAdvice
public class ControllerExceptionHandler {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@ExceptionHandler(Exception.class)
public ModelAndView exceptionHandler(HttpServletRequest req, Exception e) {
logger.error("Request : {},Exception : {}", req.getRequestURI(), e);
ModelAndView mv = new ModelAndView();
mv.addObject("url", req.getRequestURI());
mv.addObject("error", e);
mv.setViewName("error/error");
return mv;
}
}
|