转自:https://blog.csdn.net/qq_35508033/article/details/89380838
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35508033/article/details/89380838
项目地址:https://github.com/zmdlbr/toutiao
Velocity模板语法(类似Java语法)
$!{ 变量/表达式 }
## 注释 ## #* 多行注释 *#
for
#foreach ($color in $colors)
Color$!{foreach.count}/${foreach.index}:$!{color}
#end
默认目录:templates
文件:xxx.vm
属性访问
$!{user.name}
$!{user.getName()}
HttpServletResponse
response.addCookie(new Cookie(key, value));
response.addHeader(key, value);
HttpServletRequest
request.getHeaderNames();
request.getMethod()
request.getPathInfo()
request.getQueryString()
@RequestMapping(value = {"/response"})
@ResponseBody
public String response(@CookieValue(value = "nowcoderid", defaultValue = "a") String nowcoderId,
@RequestParam(value = "key", defaultValue = "key") String key,
@RequestParam(value = "value", defaultValue = "value") String value,
HttpServletResponse response) {
response.addCookie(new Cookie(key, value));
response.addHeader(key, value);
return "NowCoderId From Cookie:" + nowcoderId;
}
重定向
301:永久转移
302:临时转移
@RequestMapping("/redirect/{code}")
public String redirect(@PathVariable("code") int code,
HttpSession session) {
/*
RedirectView red = new RedirectView("/", true);
if (code == 301) {
red.setStatusCode(HttpStatus.MOVED_PERMANENTLY);
}
return red;*/
session.setAttribute("msg", "Jump from redirect.");
return "redirect:/";
}
redirect前缀,跳到首页,默认是302跳转。
从一个页面跳到另一个页面,所有的访问都是同一个HttpSession,可以在redirect中添加session的一些特性,返回到首页的时候,把session的信息读取出来,显示在首页。用户体验较好。
301是永久迁移,如果是301,会把信息存入浏览器,下次浏览器访问网址,会直接定位到另一个地方。
301是临时迁移。
AOP
面向切面,所有业务都要处理的业务
@Aspect
@Component
public class LogAspect {
private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
@Before("execution(* com.nowcoder.controller.IndexController.*(..))")
public void before(JoinPoint joinPoint) {
StringBuffer sb = new StringBuffer();
for (Object arg : joinPoint.getArgs()) {
sb.append("arg:" + arg.toString());
}
logger.info("before method:" + sb.toString());
}
@After("execution(* com.nowcoder.controller.*Controller.*(..))")
public void after(JoinPoint joinPoint) {
logger.info("after method:");
}
}
JoinPoint是个包装类,相当于面向切面的交汇点,通过getArgs方法可以获得所有的进入controller的输入参数
ViewObject:方便传递任何数据到Velocity
DateTool:velocity自带工具类导入
public class ViewObject {
private Map<String, Object> objs = new HashMap<String, Object>();
public void set(String key, Object value) {
objs.put(key, value);
}
public Object get(String key) {
return objs.get(key);
}
}
可以知道用户是这个用户。
注册成功后会进行自动登陆,对于登陆,在登陆操作中,在service层,进行逻辑判断,对上返回状态回到controller,对下dao去和数据库交互。在service登陆代码中,服务器会生成一个string类型的ticket,存入cookie中,key值是ticket,value值是ticket的值。通过response下发到浏览器。
下次在已经登陆的用户,进行其他点击后。在进入controller前,调用preHandle方法处理,它可以检查客户端提交的cookie中是否有服务器之前下发的ticket,如果有证明这个请求是已经登陆的用户了。把登陆的用户放到线程本地变量。在此时才进入controller,可以拿到具体的用户HostHolder类,这是线程本地变量,可以根据登陆的用户进行个性化渲染,比如关注用户的动态,个人收藏等
新建数据表login_ticket用来存储ticket字段。该字段在用户登录成功时被生成并存入数据库,并被设置为cookie,
下次用户登录时会带上这个ticket,ticket是随机的uuid,有过期时间以及有效状态。
使用拦截器interceptor来拦截所有用户请求,判断请求中是否有有有效的ticket,如果有的话则将用户信息写入Threadlocal。
所有线程的threadlocal都被存在一个叫做hostholder的实例中,根据该实例就可以在全局任意位置获取用户的信息。
该ticket的功能类似session,也是通过cookie写回浏览器,浏览器请求时再通过cookie传递,区别是该字段是存在数据库中的,并且可以用于移动端。
如下有个PassportInterceptor拦截器,它对于所有页面都进行处理
@Component
public class PassportInterceptor implements HandlerInterceptor{
@Autowired
private UserDAO userDAO;
@Autowired
private LoginTicketDAO loginTicketDAO;
@Autowired
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
//处理用户信息,判断是否有ticket,一个用户一个ticket,但是有时限
String ticket = null;
if (httpServletRequest.getCookies() != null) {
for (Cookie cookie : httpServletRequest.getCookies()) {
if (cookie.getName().equals("ticket")) {
ticket = cookie.getValue();
break;
}
}
//判断ticket是否过期和无效
if (ticket != null) {
LoginTicket loginTicket = loginTicketDAO.selectByTicket(ticket);
if (loginTicket == null || loginTicket.getExpired().before(new Date()) || loginTicket.getStatus() != 0) {
return true;
} else {
User user = userDAO.selectById(loginTicket.getUserId());
//将用户信息报错到当前请求线程中
hostHolder.setUsers(user);
return true;
}
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
//渲染之前提供的后处理方法,可以添加模型数据,自动传给前端
if (modelAndView != null && hostHolder.getUser() != null) {
modelAndView.addObject(hostHolder.getUser());
hostHolder.clear();
}
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
hostHolder.clear();
}
}
拦截器1放行,拦截器2 preHandle才会执行。
拦截器2 preHandle不放行,拦截器2 postHandle和afterCompletion不会执行。
只要有一个拦截器不放行,postHandle不会执行
用户数据安全性:
https可以防止运行商加塞广告,
公钥加密私钥解密,
用户密码salt防止破解
token有效期
单一平台的单点登陆,登陆IP异常检验
用户状态的权限判断
添加验证码机制,防止爆破和批量注册。
手机验证码,一瞬间一万个请求,0~9999都发到服务器上,总有一个对的上的。如果一个验证码失效,重新下发token
Spring Boot Dev Tools
动态加载更新的class
编译加载修改的静态文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
上传图片
一、post方法上传图片到服务器本地,返回UUID的string字符串,形成URl,浏览器可以输入127.0.0.1 + UUID生成的string 组成URL,后台会传回给浏览器response,带有图片的二进制流。浏览器的response中就可以看到接收的图片二进制流,显示出来。fiddler可以通过代理的方式,把网络的底层全部拦截下来
上传到七牛云,做云存储
官方文档:
https://developer.qiniu.com/kodo/sdk/1239/java#upload
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.1.1</version>
</dependency>
@Service
public class QiniuService {
private static final Logger logger = LoggerFactory.getLogger(QiniuService.class);
//设置好账号的ACCESS_KEY和SECRET_KEY
String ACCESS_KEY = "DEYWFFCReQqVMYESzB4qo9WYwYtxmVf-8DHU34Jr";
String SECRET_KEY = "5bFWFSYb9Gyx5v-5wYnket58cashXLw2-aZ6D3g2";
//要上传的空间
String bucketname = "toutiao";
//密钥配置
Auth auth = Auth.create(ACCESS_KEY, SECRET_KEY);
//创建上传对象
UploadManager uploadManager = new UploadManager();
private static String QINIU_IMAGE_DOMAIN = "http://p8aa1ssg4.bkt.clouddn.com/";
//简单上传,使用默认策略,只需要设置上传的空间名就可以了
public String getUpToken() {
return auth.uploadToken(bucketname);
}
public String saveImage(MultipartFile file) throws IOException {
try {
int dotPos = file.getOriginalFilename().lastIndexOf(".");
if (dotPos < 0) {
return null;
}
String fileExt = file.getOriginalFilename().substring(dotPos + 1).toLowerCase();
if (!ToutiaoUtil.isFileAllowed(fileExt)) {
return null;
}
String fileName = UUID.randomUUID().toString().replaceAll("-", "") + "." + fileExt;
//调用put方法上传
Response res = uploadManager.put(file.getBytes(), fileName, getUpToken());
//打印返回的信息
if (res.isOK() && res.isJson()) {
return QINIU_IMAGE_DOMAIN + JSONObject.parseObject(res.bodyString()).get("key");
} else {
logger.error("七牛异常:" + res.bodyString());
return null;
}
} catch (QiniuException e) {
// 请求失败时打印的异常的信息
logger.error("七牛异常:" + e.getMessage());
return null;
}
}
}
云可以做实时缩图和实时切图
云实时缩图
阿里云
http://images.nowcoder.com/images/20150205/60_1423125965233_60_1423125960758_%E7
%AE%A1%E7%90%86%E5%91%98%E5%A4%B4%E5%83%8F.png@0e_100w_100h_0c_1i_1o_9
0Q_1x.png
七牛云
http://7xsetu.com1.z0.glb.clouddn.com/300_300.png?imageView2/1/w/100/h/100/
http://developer.qiniu.com/code/v6/api/kodo-api/image/imageview2.html
评论中心
按常规思路 评论的数据库创建字段有 id、content、user_id、created_date、status、news_id这些。这里数据库的设计就有点像程序设计中的面向接口编程还是面向实体编程。如果字段中有news_id的话,本意是将评论和新闻关联起来,但是评论不光只在新闻下有,在别人的评论下也可以有评论。所以字段要设计成如下,将news_id替换成entity_id, entity_type。entity_id可以表示news_id,也可以表示成 comment_id。entity_type可以是news,也可以代表comment。这里我们可以做好如下约定。entity_type为1表示是新闻,entity_id是新闻id;2表示为comment,表示评论的评论
@Service
public class JedisAdapter implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(JedisAdapter.class);
public static void print(int index, Object obj) {
System.out.println(String.format("%d,%s", index, obj.toString()));
}
// public static void main(String[] args) {
// Jedis jedis = new Jedis();
// jedis.flushAll();
// // get,set
// jedis.set("hello", "world");
// print(1, jedis.get("hello"));
// jedis.rename("hello", "newhello");
// print(1, jedis.get("newhello"));
// jedis.setex("hello2", 15, "world");
//
// // 数值操作
// jedis.set("pv", "100");
// jedis.incr("pv");
// jedis.decrBy("pv", 5);
// print(2, jedis.get("pv"));
// print(3, jedis.keys("*"));
//
// // 列表操作, 最近来访, 粉丝列表,消息队列
// String listName = "list";
// jedis.del(listName);
// for (int i = 0; i < 10; ++i) {
// jedis.lpush(listName, "a" + String.valueOf(i));
// }
// print(4, jedis.lrange(listName, 0, 12)); // 最近来访10个id
// print(5, jedis.llen(listName));
// print(6, jedis.lpop(listName));
// print(7, jedis.llen(listName));
// print(8, jedis.lrange(listName, 2, 6)); // 最近来访10个id
// print(9, jedis.lindex(listName, 3));
// print(10, jedis.linsert(listName, BinaryClient.LIST_POSITION.AFTER, "a4", "xx"));
// print(10, jedis.linsert(listName, BinaryClient.LIST_POSITION.BEFORE, "a4", "bb"));
// print(11, jedis.lrange(listName, 0, 12));
//
//
// // hash, 可变字段
// String userKey = "userxx";
// jedis.hset(userKey, "name", "jim");
// jedis.hset(userKey, "age", "12");
// jedis.hset(userKey, "phone", "18666666666");
// print(12, jedis.hget(userKey, "name"));
// print(13, jedis.hgetAll(userKey));
// jedis.hdel(userKey, "phone");
// print(14, jedis.hgetAll(userKey));
// print(15, jedis.hexists(userKey, "email"));
// print(16, jedis.hexists(userKey, "age"));
// print(17, jedis.hkeys(userKey));
// print(18, jedis.hvals(userKey));
// jedis.hsetnx(userKey, "school", "zju");
// jedis.hsetnx(userKey, "name", "yxy");
// print(19, jedis.hgetAll(userKey));
//
// // 集合,点赞用户群, 共同好友
// String likeKey1 = "newsLike1";
// String likeKey2 = "newsLike2";
// for (int i = 0; i < 10; ++i) {
// jedis.sadd(likeKey1, String.valueOf(i));
// jedis.sadd(likeKey2, String.valueOf(i * 2));
// }
// print(20, jedis.smembers(likeKey1));
// print(21, jedis.smembers(likeKey2));
// print(22, jedis.sunion(likeKey1, likeKey2));
// print(23, jedis.sdiff(likeKey1, likeKey2));
// print(24, jedis.sinter(likeKey1, likeKey2));
// print(25, jedis.sismember(likeKey1, "12"));
// print(26, jedis.sismember(likeKey2, "12"));
// jedis.srem(likeKey1, "5");
// print(27, jedis.smembers(likeKey1));
// // 从1移动到2
// jedis.smove(likeKey2, likeKey1, "14");
// print(28, jedis.smembers(likeKey1));
// print(29, jedis.scard(likeKey1));
//
// // 排序集合,有限队列,排行榜
// String rankKey = "rankKey";
// jedis.zadd(rankKey, 15, "Jim");
// jedis.zadd(rankKey, 60, "Ben");
// jedis.zadd(rankKey, 90, "Lee");
// jedis.zadd(rankKey, 75, "Lucy");
// jedis.zadd(rankKey, 80, "Mei");
// print(30, jedis.zcard(rankKey));
// print(31, jedis.zcount(rankKey, 61, 100));
// // 改错卷了
// print(32, jedis.zscore(rankKey, "Lucy"));
// jedis.zincrby(rankKey, 2, "Lucy");
// print(33, jedis.zscore(rankKey, "Lucy"));
// jedis.zincrby(rankKey, 2, "Luc");
// print(34, jedis.zscore(rankKey, "Luc"));
// print(35, jedis.zcount(rankKey, 0, 100));
// // 1-4 名 Luc
// print(36, jedis.zrange(rankKey, 0, 10));
// print(36, jedis.zrange(rankKey, 1, 3));
// print(36, jedis.zrevrange(rankKey, 1, 3));
// for (Tuple tuple : jedis.zrangeByScoreWithScores(rankKey, "60", "100")) {
// print(37, tuple.getElement() + ":" + String.valueOf(tuple.getScore()));
// }
//
// print(38, jedis.zrank(rankKey, "Ben"));
// print(39, jedis.zrevrank(rankKey, "Ben"));
//
// String setKey = "zset";
// jedis.zadd(setKey, 1, "a");
// jedis.zadd(setKey, 1, "b");
// jedis.zadd(setKey, 1, "c");
// jedis.zadd(setKey, 1, "d");
// jedis.zadd(setKey, 1, "e");
// print(40, jedis.zlexcount(setKey, "-", "+"));
// print(41, jedis.zlexcount(setKey, "(b", "[d"));
// print(42, jedis.zlexcount(setKey, "[b", "[d"));
// jedis.zrem(setKey, "b");
// print(43, jedis.zrange(setKey, 0, 10));
// jedis.zremrangeByLex(setKey, "(c", "+");
// print(44, jedis.zrange(setKey, 0, 2));
//
// /*
// jedis.lpush("aaa", "A");
// jedis.lpush("aaa", "B");
// jedis.lpush("aaa", "C");
// print(45, jedis.brpop(0, "aaa"));
// print(45, jedis.brpop(0, "aaa"));
// print(45, jedis.brpop(0, "aaa"));
// */
//
//
// JedisPool pool = new JedisPool();
// for (int i = 0; i < 100; ++i) {
// Jedis j = pool.getResource();
// j.get("a");
// j.close();
// }
// }
private Jedis jedis = null;
private JedisPool pool = null;
@Override
public void afterPropertiesSet() throws Exception {
//jedis = new Jedis("localhost");
pool = new JedisPool("localhost", 6379);
}
private Jedis getJedis() {
//return jedis;
return pool.getResource();
}
public String get(String key) {
Jedis jedis = null;
try {
jedis = pool.getResource();
return getJedis().get(key);
} catch (Exception e) {
logger.error("发生异常" + e.getMessage());
return null;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
public void set(String key, String value) {
Jedis jedis = null;
try {
jedis = pool.getResource();
jedis.set(key, value);
} catch (Exception e) {
logger.error("发生异常" + e.getMessage());
} finally {
if (jedis != null) {
jedis.close();
}
}
}
public long sadd(String key, String value) {
Jedis jedis = null;
try {
jedis = pool.getResource();
return jedis.sadd(key, value);
} catch (Exception e) {
logger.error("发生异常" + e.getMessage());
return 0;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
public long srem(String key, String value) {
Jedis jedis = null;
try {
jedis = pool.getResource();
return jedis.srem(key, value);
} catch (Exception e) {
logger.error("发生异常" + e.getMessage());
return 0;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
public boolean sismember(String key, String value) {
Jedis jedis = null;
try {
jedis = pool.getResource();
return jedis.sismember(key, value);
} catch (Exception e) {
logger.error("发生异常" + e.getMessage());
return false;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
public long scard(String key) {
Jedis jedis = null;
try {
jedis = pool.getResource();
return jedis.scard(key);
} catch (Exception e) {
logger.error("发生异常" + e.getMessage());
return 0;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
public void setex(String key, String value) {
// 验证码, 防机器注册,记录上次注册时间,有效期3天
Jedis jedis = null;
try {
jedis = pool.getResource();
jedis.setex(key, 10, value);
} catch (Exception e) {
logger.error("发生异常" + e.getMessage());
} finally {
if (jedis != null) {
jedis.close();
}
}
}
public long lpush(String key, String value) {
Jedis jedis = null;
try {
jedis = pool.getResource();
return jedis.lpush(key, value);
} catch (Exception e) {
logger.error("发生异常" + e.getMessage());
return 0;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
public List<String> brpop(int timeout, String key) {
Jedis jedis = null;
try {
jedis = pool.getResource();
return jedis.brpop(timeout, key);
} catch (Exception e) {
logger.error("发生异常" + e.getMessage());
return null;
} finally {
if (jedis != null) {
jedis.close();
}
}
}
public void setObject(String key, Object obj) {
set(key, JSON.toJSONString(obj));
}
public <T> T getObject(String key, Class<T> clazz) {
String value = get(key);
if (value != null) {
return JSON.parseObject(value, clazz);
}
return null;
}
}
Redis在牛客
PV //讨论区里的浏览数在Redis中存储
点赞 //点赞,userId放在Redis中
关注 //人与人之间关注,关注者是个集合,放进Redis,如果取消关注,从集合中删除即可
排行榜 //登陆数,登陆一次,数值加1
验证码 //验证码,用到set timeout时间,后台生成验证码,放进Redis,设置过期时间,比如三分钟还没收到用户验证码,自动从Redis删除验证码。
缓存 //牛客网上访问,打开网页用户头像,用户名昵称,如果用户不更新,用户信息等在MySQL上可以做一层缓存,如果用户没更新信息,可以直接从缓存读,如果缓存没有,直接去数据库取,大大降低数据库的压力。
异步队列 //点了赞,谁评论了帖子,发生事件,放进redis,后面有线程去执行
判题队列 //用户提交判题,放入判题队列,在Redis中,服务器连判题队列执行,如果碰到高峰期,可以加机器
Redis可以设置有效期,在登陆的时候可以用到,把用户登陆服务器下发的token存到Redis中,设置过期时间,时间到自动删除了。比存在数据库中判断expired_time更方便
List:双向列表,适用于最新列表,关注列表
lpush
lpop
blpop
lindex
lrange
lrem
linsert
lset
rpush
Set:适用于无顺序的集合,点赞点
踩,抽奖,已读,共同好友
sdiff
smembers
sinter
scard
SortedSet:排行榜,优先队列
zadd
zscore
zrange
zcount
zrank
zrevrank
Hash:对象属性,不定长属性数
hset
hget
hgetAll
hexists
hkeys
hvals
KV:单一数值,验证码,PV,缓存
set
setex
incr
异步队列
点赞,回复评论的时候,表面上是赞数增加了,其实还有很多其他的工作要做。比如,对方要收到消息提醒,成就值增加。一些行为会引起一系列连锁反应。如果在点赞时立马处理,会影响程序运行效率。而且会造成代码冗余,比如发布新闻,和回复评论都可以使得成就值增加,如果都跟着写在后面的代码里会把成就值增加这段代码写两遍,所以大型服务需要服务化和异步化。
利用JSON进行对象序列化和反序列化,JSON存到Redis中,可以做在在Redis中存储对象
一、在controller中执行like喜欢动作的时候,会有一个EventModel模型,记录当下事件的所有信息,json序列化,存入redis缓存中。EventModel相关的属性有,活动类型EventType,触发者,出发对象id,触发对象类型,触发对象拥有者,触发现场有哪些信息要保存map。
二、后台一直有一个线程在消费给队列。如何实现呢,考虑到拓展性,首先会定义一个service层的接口,EventHandler,定义其中的方法doHandler要做的事情和当前event涉及到的所有活动类型EventType。
三、实现EventHandler接口,如有点赞活动,我点赞后,需要执行什么,有LikeHandler实现EventHandler接口,在doHandler中写具体要执行的方法,比如点赞后在后台可以发消息给用户。点赞相关的活动只有点赞。复杂一点的活动会涉及好几个活动类型。
四、最重要的是EventConsumer,实现了InitializingBean, ApplicationContextAware接口,ApplicationContextAware可以获得当前的applicationContext,之后是InitializingBean接口中的afterPropertiesSet方法,在里面配置好config。
首先拿到所有实现EventHandler接口的所有类。下面起一个线程从队列中消费。
---------------------
作者:近忧12138
来源:CSDN
原文:https://blog.csdn.net/qq_35508033/article/details/89380838
版权声明:本文为博主原创文章,转载请附上博文链接!