目录
1. 项目实现功能
2. 项目整体架构
2.1 架构及内容
2.2 功能:
2.3 业务逻辑
2.3.1 注册、登录、登出
2.3.2 拦截器
2.3.3 图片上传
2.3.4 发布news和comment
2.3.5 Redis实现点赞点踩
2.3.6 异步处理
数据库中表:
统一的数据格式:{code:0,msg:”,data:”} :使用fastJson返回Json格式的结果{code:"xx",message:"xxxxxx"}; controller方法使用@ResponseBody注解
注册用户;传参:username、password、remberme
用户登录;
用户登出/logout/
设置ticket状态为1(失效);
PassportInterceptor拦截器,它对于所有页面都进行处理
1. 可以知道用户是哪个用户。
注册成功后会进行自动登陆,对于登陆,在登陆操作中,在service层,进行逻辑判断,对上返回状态回到controller,对下dao去和数据库交互。在service登陆代码中,服务器会生成一个string类型的ticket,存入cookie中,key值是ticket,value值是ticket的值。通过response下发到浏览器。
下次在已经登陆的用户,进行其他点击后。在进入controller前,调用preHandle方法处理,它可以检查客户端提交的cookie中是否有服务器之前下发的ticket,如果有证明这个请求是已经登陆的用户了。把登陆的用户放到线程本地变量。在此时才进入controller,可以拿到具体的用户HostHolder类,这是线程本地变量,可以根据登陆的用户进行个性化渲染,比如关注用户的动态,个人收藏等。
二、可以进行权限管理
某些页面需要用户已登录(比如:站内信页面)或者用户是某个等级的。可以现在preHandler中判断,这个可以在设置一个特定范围的拦截器,如下的拦截器LoginRequiredInterceptor,在访问setting页面时才会进入到该拦截器,如果验证没有HostHolder,说明用户没登陆(没有hostholder说明该用户未登录,或ticket已过期/已失效),就跳转到主页或者给出登陆页面。拦截器也先后执行顺序。在配置类中ToutiaoWebConfiguration中先后决定。
三、可以自动登陆
用户请求中cookie包含的sessionId(就是ticket),服务端的拦截器判断该sessionId是否和服务器的ticket关联起来,如果关联,且ticket未过期、未失效时,那么就会知道该请求对应的是哪一个用户,可以在ModelAndView中添加相应的user,可以做到自动登录的功能。
MultipartFile接口
一、post方法上传图片到服务器本地,返回访问图片的url
public String saveImage(MultipartFile file) throws IOException {
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;
Files.copy(file.getInputStream(), new File(ToutiaoUtil.IMAGE_DIR + fileName).toPath(),
StandardCopyOption.REPLACE_EXISTING);
return ToutiaoUtil.TOUTIAO_DOMAIN + "image?name=" + fileName;
}
图片访问:
通过图片的url,服务器使用StreamUtils.copy()将对应的图片二进制流,copy到response的输出流中。
@RequestMapping(path = {"/image"}, method = {RequestMethod.GET})
@ResponseBody
public void getImage(@RequestParam("name") String imageName,
HttpServletResponse response) {
try {
response.setContentType("image/jpeg");
StreamUtils.copy(new FileInputStream(new
File(ToutiaoUtil.IMAGE_DIR + imageName)), response.getOutputStream());
} catch (Exception e) {
logger.error("读取图片错误" + imageName + e.getMessage());
}
}
二、上传到七牛云,做云存储
优势:图片做单独的服务器
cdn内容分发网络,内容分发到各个节点,能够更快的访问静态文件。
云可以做实时缩图和实时切图。
七牛云上传图片直接写成一个service,如果要更换云服务商,如阿里云,可以直接改service就行。
/user/addNews/:用户发布news
如果用户为登录用户(hostHolder.getUser()!=null),则该news设定为对应的用户,;
如果用户为未登录用户,则设定为匿名用户(news.setUserId(3);)
/news/{newsId}:newsid对应的news详情页;
1. news内容:获取该newsId对应的news内容;
2. 赞的状态:用户为登录用户,获取用户对该news的赞状态;
3. 评论:获取该news对应的评论;
/addComment:添加newsId对应的一个comment;
1. 添加comment;
2. 更新news相关的评论数目;
使用redis的set数据结构实现;
好处:Redis性能好,快,并发高;在点赞过后要立即刷新显示在页面,所以推荐使用Redis。
RedisUtil类中方法:
思路:
消息队列MQ主要是用来:
目前使用的较多的有ActiveMQ、RabbitMQ、ZeroMQ、Kafka、MetaMQ、RocketMQ等。
Redis不仅可作为缓存服务器,还可用作消息队列。它的list类型天生支持用作消息队列。list是使用双向链表实现的,保存了头尾节点,所以在列表头尾两边插取元素都是非常快的。
为什么要用Redis实现轻量级MQ?
在业务的实现过程中,就算没有大量的流量,解耦和异步化几乎也是处处可用,此时MQ就显得尤为重要。但与此同时MQ也是一个蛮重的组件,例如我们如果用RabbitMQ就必须为它搭建一个服务器,同时如果要考虑可用性,就要为服务端建立一个集群,而且在生产如果有问题也需要查找功能。在中小型业务的开发过程中,可能业务的其他整个实现都没这个重。过重的组件服务会成倍增加工作量。
所幸的是,Redis提供的list数据结构非常适合做消息队列。
Fastjson 是一个 Java 库,可以将 Java 对象转换为 JSON 格式,当然它也可以将 JSON 字符串转换为 Java 对象。调用toJSONString方 法即可将对象转换成 JSON 字符串,parseObject 方法则反过来将 JSON 字符串转换成对象
EventProducer:生产方
@Service
public class EventProducer {
@Autowired
JedisAdapter jedisAdapter;
public boolean fireEvent(EventModel model) {
try {
String json = JSONObject.toJSONString(model);
String key = RedisKeyUtil.getEventQueueKey();
jedisAdapter.lpush(key, json);
return true;
} catch (Exception e) {
return false;
}
}
}
EventConsumer类:消费者
Map beans = applicationContext.getBeansOfType(EventHandler.class);
2. 根据EventHandler的实现类,得到每个EventType对应的处理器(EventHandle)是哪些;
3. 新建线程,while(true)循环使得后台一直有一个线程在消费队列。 从list中读取对应key下的事件(即EventModule对象),根据该EventModule对象的EventType,调用对应的EventHandle来处理事件。
@Service
public class EventConsumer implements InitializingBean, ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);
private Map> config = new HashMap>();
private ApplicationContext applicationContext;
@Autowired
JedisAdapter jedisAdapter;
@Override
public void afterPropertiesSet() throws Exception {
//获取EventHandler类所有的实现类的bean,返回一个map类型的实例,map中的key为bean的名字,key对应的内容未bean的实例。
Map beans = applicationContext.getBeansOfType(EventHandler.class);
if (beans != null) {
//得到每个EventType对应的EventHandle
for (Map.Entry entry : beans.entrySet()) {
List eventTypes = entry.getValue().getSupportEventTypes();
for (EventType type : eventTypes) {
if (!config.containsKey(type)) {
config.put(type, new ArrayList());
}
config.get(type).add(entry.getValue());
}
}
}
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
String key = RedisKeyUtil.getEventQueueKey();
List events = jedisAdapter.brpop(0, key);
for (String message : events) {
if (message.equals(key)) {
continue;
}
EventModel eventModel = JSON.parseObject(message, EventModel.class);
if (!config.containsKey(eventModel.getType())) {
logger.error("不能识别的事件");
continue;
}
for (EventHandler handler : config.get(eventModel.getType())) {
handler.doHandle(eventModel);
}
}
}
}
});
thread.start();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
InitializingBean接口:afterPropertiesSet方法,初始化bean的时候执行,可以针对某个具体的bean进行配置;实现 InitializingBean接口必须实现afterPropertiesSet方法。
ApplicationContextAware接口:某些特殊的情况下,Bean需要实现某个功能,但该功能必须借助于Spring容器才能实现,此时就必须让该Bean先获取Spring容器,然后借助于Spring容器实现该功能。为了让Bean获取它所在的Spring容器,可以让该Bean实现ApplicationContextAware接口。
详见:Bean的生命周期