# 旧Servlet + JSP用户管理项目
沐颜科技
在线用户剔除操作
WEB应用是一个多用户的使用环境,每一个用户都通过自己的session进行个人数据的记录,用户也可以手工进行注销操作,以实现session资源的释放,但是在一些系统中为了便于用户的管理,往往需要进行用户手工强制注销操作
# 管理程序清单
No. | 程序文件名称 | 类型 | 描述 |
---|---|---|---|
1 | /login.jsp | JSP | 提供登录表单,以及错误显示 |
2 | /pages/admin/online_user_list.jsp | JSP | 管理员查看在线用户列表信息 |
3 | /pages/front/welcome.jsp | JSP | 用户登录成功后的欢迎页面 |
4 | com.yootk.servlet.LoginServlet | Servlet | 用户登录处理程序,密码为yootk表示登录成功 |
5 | com.yootk.servlet.KickoutServlet | Servlet | 在线用户剔除处理Servlet |
6 | com.yootk.listener.OnlineListener | Listener | 监听器,在用户登录成功或注销后更新用户列表 |
7 | com.yootk.filter.InvalidateFilter | Filter | 登录失效检查,如果发现登录失效则跳转到登录页 |
8 | com.yootk.filter.EncodingFilter | Filter | 编码过滤器 |
# 登录
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String id = req.getParameter("userid"); // 接收请求参数
String password = req.getParameter("password"); // 接收请求参数
if (!"yootk".equals(password)) { // 密码输入错误
req.setAttribute("error", "错误的用户名及密码!");
req.getRequestDispatcher("/errors.jsp").forward(req, resp);
}
// WEB组件中提供有监听操作,而通过监听操作可以实现HttpSession属性的处理
req.getSession().setAttribute("userid", id); // 设置Session属性内容
resp.sendRedirect("/pages/front/welcome.jsp"); // 客户端跳转
}
}
需要注意的是,此时并不是要完成一个简单的用户登录,而是需要对用户登录的状态进行
监听,因为最终要将登录的结果保
存在session属性之中,而session的内容又需要保存在application属性之中. .
# 保存登录信息
@WebListener
public class OnlineListener implements ServletContextListener, HttpSessionListener, HttpSessionAttributeListener {
private ServletContext servletContext; // 获取application实例
@Override
public void contextInitialized(ServletContextEvent sce) { // 进行初始化集合存储
this.servletContext = sce.getServletContext(); // 存在有公共的application属性
// Map集合之中的key表示用户名,而Value是保存当前的用户状态
this.servletContext.setAttribute("online", new HashMap<String, Boolean>());
}
@Override
public void attributeAdded(HttpSessionBindingEvent se) {
// 用户登录成功会设置session的属性内容,这样就会触发本方法的执行
if ("userid".equals(se.getName())) { // 判断是否为指定的属性操作
Map<String, Boolean> map = (Map<String, Boolean>) this.servletContext.getAttribute("online");
map.put((String)se.getValue(), false); // 保存用户信息
this.servletContext.setAttribute("online", map);
}
}
@Override
public void sessionDestroyed(HttpSessionEvent se) { // 用户离开了
Map<String, Boolean> map = (Map<String, Boolean>) this.servletContext.getAttribute("online");
map.remove(se.getSession().getAttribute("userid")); // 删除数据
this.servletContext.setAttribute("online", map);
}
}
# 在线用户列表
%@ page pageEncoding="UTF-8" import="java.util.*" %>
<% // 通过request获取相关资源信息,拼凑成完整的访问路径
String basePath = request.getScheme() + "://" + request.getServerName() + ":" +
request.getServerPort() + request.getContextPath() + "/" ;
%>
<html>
<head>
<title>沐言科技:www.yootk.com</title>
<base href="<%=basePath%>">
<meta name="viewport" content="width=device-width,initial-scale=1">
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="bootstrap/js/bootstrap.min.js"></script>
<link rel="stylesheet" type="text/css" href="bootstrap/css/bootstrap.min.css" />
</head>
<%!
public static final String KICKOUT_URL = "/pages/admin/KickoutServlet";
%>
<body class="container">
<div class="row">
<div class="panel panel-success">
<div class="panel-heading">
<strong><i class="fa fa-archive"></i> 在线用户列表</strong>
</div>
<div class="panel-body">
<% // 获取当前全部的用户信息
Map<String, Boolean> onlineMap = (Map<String, Boolean>) application.getAttribute("online"); // 获取Map集合
%>
<table class="table table-hover">
<tr>
<th width="40%" class="text-center">用户名</th>
<th width="20%" class="text-center">状态</th>
<th width="10%" class="text-center">操作</th>
</tr>
<%
for (Map.Entry<String, Boolean> entry : onlineMap.entrySet()) {
%>
<tr>
<td class="text-center"><%=entry.getKey()%></td>
<td class="text-center">
<%
if (entry.getValue()) {
%>
<span class="label label-default">剔除</span>
<%
} else {
%>
<span class="label label-success">在线</span>
<%
}
%>
</td>
<td class="text-center">
<a class="btn btn-xs btn-danger" href="<%=KICKOUT_URL%>?userid=<%=entry.getKey()%>">
<span class="glyphicon glyphicon-remove"></span> 强制下线</a></td>
</tr>
<%
}
%>
</table>
</div>
<div class="panel-footer">
<div style="text-align:right;">
<img src="images/logo.png" style="height: 30px;">
<strong>沐言科技(www.yootk.com) —— 新时代软件教育领导品牌</strong>
</div>
</div>
</div>
</div>
</body>
</html>
# 注销
@WebServlet("/pages/admin/KickoutServlet")
public class KickoutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String userid = req.getParameter("userid");
Map<String, Boolean> onlineMap = (Map<String, Boolean>) req.getServletContext().getAttribute("online"); // Map集合
if (onlineMap.containsKey(userid)) { // 用户还没走呢
onlineMap.put(userid, true); // 设置为True就表示要剔除了
}
req.getServletContext().setAttribute("online", onlineMap);
resp.sendRedirect("/pages/admin/online_user_list.jsp");
}
}
能够让用户注销的操作只能够是用户本身完成的事情,所以可以考虑做一个过滤器,过滤
器在用户每次访问的时候都判断一
下用户的状态,如果发现用户已经被删除了,则强制性的注销.
@WebFilter("/pages/front/*")
public class InvalidateFilter extends HttpFilter {
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String userid = (String) request.getSession().getAttribute("userid");
if (userid != null) { // 用户登录过
Map<String, Boolean> onlineMap = (Map<String, Boolean>) request.getServletContext().getAttribute("online"); // Map集合
if (onlineMap.containsKey(userid)) { // 用户还存在
if (onlineMap.get(userid) == true) { // 本身保存的就是布尔类型
request.getSession().invalidate(); // 强制注销
request.setAttribute("error", "您的账户已被系统强制下线,为了您的安全,请重新登录!");
request.getRequestDispatcher("/login.jsp").forward(request, response);
} else { // 正常的状态
chain.doFilter(request, response);
}
}
} else {
request.setAttribute("error", "您还未登录,请先登录!");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
}
# 旧Servlet SSM项目
# 用户登录和注销接口开发
//controller层代码
public void login(HttpServletRequest request,HttpServletResponse response){
String phone = request.getParameter("phone");
String pwd = request.getParameter("pwd");
User user = userService.login(phone,pwd);
if(user != null){
request.getSession().setAttribute("loginUser",user);
//跳转页面
}else {
request.setAttribute("msg","用户名或者密码不正确");
}
}
//service层代码
@Override
public User login(String phone, String pwd) {
String md5pwd = CommonUtil.MD5(pwd);
User user = userDao.findByPhoneAndPwd(phone,md5pwd);
return user;
}
//dao层代码
public User findByPhoneAndPwd(String phone, String md5pwd) {
String sql = "select * from user where phone=? and pwd=?";
User user = null;
try{
user = queryRunner.query(sql,new BeanHandler<>(User.class,processor),phone,md5pwd);
}catch (Exception e){
e.printStackTrace();
}
return user;
}
# 发布主题接口
//controller层代码
/**
* http://localhost:8080/topic?method=addTopic
* 发布主题
* @param request
* @param response
*/
public void addTopic(HttpServletRequest request,HttpServletResponse response){
User loginUser = (User)request.getSession().getAttribute("loginUser");
if(loginUser == null){
request.setAttribute("msg","请登录");
return;
//页面跳转 TODO
}
String title = request.getParameter("title");
String content = request.getParameter("content");
int cId = Integer.parseInt(request.getParameter("c_id"));
int rows = topicService.addTopic(loginUser,title,content,cId);
if(rows ==1){
//发布主题成功
}else {
//发布主题失败
}
}
//service层代码
@Override
public int addTopic(User loginUser, String title, String content, int cId) {
Category category = categoryDao.findById(cId);
if(category == null){ return 0;}
Topic topic = new Topic();
topic.setTitle(title);
topic.setContent(content);
topic.setCreateTime(new Date());
topic.setUpdateTime(new Date());
topic.setPv(1);
topic.setDelete(0);
topic.setUserId(loginUser.getId());
topic.setUsername(loginUser.getUsername());
topic.setUserImg(loginUser.getImg());
topic.setcId(cId);
topic.setHot(0);
int rows = 0;
try {
rows = topicDao.save(topic);
} catch (Exception e) {
e.printStackTrace();
}
return rows;
}
//dao层代码
public int save(Topic topic) throws Exception {
String sql = "insert into topic(c_id,title,content,pv,user_id,username,user_img,create_time,update_time,hot,`delete`) " +"values(?,?,?,?,?,?,?,?,?,?,?)";
Object [] params = {
topic.getcId(),
topic.getTitle(),
topic.getContent(),
topic.getPv(),
topic.getUserId(),
topic.getUsername(),
topic.getUserImg(),
topic.getCreateTime(),
topic.getUpdateTime(),
topic.getHot(),
topic.getDelete()
};
int i =0;
try{
i= queryRunner.update(sql,params);
}catch (Exception e){
e.printStackTrace();
throw new Exception();
}
return i;
}
# 回复盖楼功能开发
- Servlet-Service-Dao层开发
//controller层代码
/**
* http://localhost:8080/topic?method=replyByTopicId&topic_id=9
* 盖楼回复
* @param request
* @param response
*/
public void replyByTopicId(HttpServletRequest request,HttpServletResponse response){
User loginUser = (User)request.getSession().getAttribute("loginUser");
if(loginUser == null){
request.setAttribute("msg","请登录");
return;
//页面跳转 TODO
}
int topicId = Integer.parseInt(request.getParameter("topic_id"));
String content = request.getParameter("content");
int rows = topicService.replyByTopicId(loginUser,topicId,content);
if(rows ==1){
//回复成功
}else {
//回复失败
}
}
//service层代码
@Override
public int replyByTopicId(User loginUser, int topicId, String content) {
int floor = topicDao.findLatestFloorByTopicId(topicId);
Reply reply = new Reply();
reply.setContent(content);
reply.setCreateTime(new Date());
reply.setUpdateTime(new Date());
reply.setFloor(floor+1);
reply.setTopicId(topicId);
reply.setUserId(loginUser.getId());
reply.setUsername(loginUser.getUsername());
reply.setUserImg(loginUser.getImg());
reply.setDelete(0);
int rows = replyDao.save(reply);
return rows;
}
//dao层代码
public int save(Reply reply) {
String sql = "insert into reply (topic_id,floor,content,user_id,username,user_img,create_time,update_time,`delete`)" +
"values (?,?,?,?,?,?,?,?,?)";
Object [] params = {
reply.getTopicId(),
reply.getFloor(),
reply.getContent(),
reply.getUserId(),
reply.getUsername(),
reply.getUserImg(),
reply.getCreateTime(),
reply.getUpdateTime(),
reply.getDelete()
};
int rows = 0;
try{
rows = queryRunner.update(sql,params);
}catch (Exception e){
e.printStackTrace();
}
return rows;
}
# topic阅读量递增
- 通过session和topic进行绑定,一个session访问同个topic只算一次阅读
//controller层代码
//处理浏览量,如果同个session内只算一次
String sessionReadKey = "is_read_"+topicId;
Boolean isRead = (Boolean) request.getSession().getAttribute(sessionReadKey);
if(isRead == null){
request.getSession().setAttribute(sessionReadKey,true);
//新增一个pv
topicService.addOnePV(topicId);
}
//service层代码
@Override
public void addOnePV(int topicId) {
Topic topic = topicDao.findById(topicId);
int newPV = topic.getPv()+1;
topicDao.updatePV(topicId,newPV,topic.getPv());
}
//dao层代码
/**
* 更新浏览量
*/
public int updatePV(int topicId, int newPV, int oldPV) {
String sql = "update topic set pv=? where pv=? and id=?";
int rows = 0;
try {
rows = queryRunner.update(sql,newPV,oldPV,topicId);
}catch (Exception e){
e.printStackTrace();
}
return rows;
}
# 页自动跳转
简介:小滴课堂首页自动跳转配置
- home.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>小滴课堂开发者论坛</title>
</head>
<body>
<jsp:forward page="/topic?method=list&c_id=1"></jsp:forward>
</body>
</html>
- web.xml
<welcome-file-list>
<welcome-file>home.jsp</welcome-file>
</welcome-file-list>
# 用户登录校验拦截器开发
开发对应的登录拦截器
开发loginInterceptor
-
登录校验成功放行
/** * 进入到controller之前的方法 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { try { String accesToken = request.getHeader("token"); if (accesToken == null) { accesToken = request.getParameter("token"); } if (StringUtils.isNotBlank(accesToken)) { Claims claims = JWTUtils.checkJWT(accesToken); if (claims == null) { //告诉登录过期,重新登录 sendJsonMessage(response, JsonData.buildError("登录过期,重新登录")); return false; } Integer id = (Integer) claims.get("id"); String name = (String) claims.get("name"); request.setAttribute("user_id", id); request.setAttribute("name", name); return true; } }catch (Exception e){} sendJsonMessage(response, JsonData.buildError("登录过期,重新登录")); return false; }
-
登录不成功返回json数据
/** * 响应json数据给前端 * @param response * @param obj */ public static void sendJsonMessage(HttpServletResponse response, Object obj){ try{ ObjectMapper objectMapper = new ObjectMapper(); response.setContentType("application/json; charset=utf-8"); PrintWriter writer = response.getWriter(); writer.print(objectMapper.writeValueAsString(obj)); writer.close(); response.flushBuffer(); }catch (Exception e){ e.printStackTrace(); } }
# loginInterceptor注册和放行路径
loginInterceptor 拦截器注册和路径校验配置
- 继承 WebMvcConfigurer
- 配置拦截路径和放行路径
/**
* 拦截器配置
*
* 不用权限可以访问url /api/v1/pub/
* 要登录可以访问url /api/v1/pri/
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Bean
LoginInterceptor loginInterceptor(){
return new LoginInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截全部
registry.addInterceptor(loginInterceptor()).addPathPatterns("/api/v1/pri/*/*/**")
//不拦截哪些路径 斜杠一定要加
.excludePathPatterns("/api/v1/pri/user/login","/api/v1/pri/user/register");
WebMvcConfigurer.super.addInterceptors(registry);
}
}
# Guava Cache缓存
# 谷歌开源缓存框架Guava Cache讲解和封装缓存组件
-
添加依赖
<!--guava依赖包--> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>19.0</version> </dependency>
-
封装api
private Cache<String,Object> tenMinuteCache = CacheBuilder.newBuilder() //设置缓存初始大小,应该合理设置,后续会扩容 .initialCapacity(10) //最大值 .maximumSize(100) //并发数设置 .concurrencyLevel(5) //缓存过期时间,写入后10分钟过期 .expireAfterWrite(600,TimeUnit.SECONDS) //统计缓存命中率 .recordStats() .build(); public Cache<String, Object> getTenMinuteCache() { return tenMinuteCache; } public void setTenMinuteCache(Cache<String, Object> tenMinuteCache) { this.tenMinuteCache = tenMinuteCache; }
# 轮播图接口引入本地缓存
- 轮播图接口加入缓存
try{
Object cacheObj = baseCache.getTenMinuteCache().get(CacheKeyManager.INDEX_BANNER_KEY, ()->{
List<VideoBanner> bannerList = videoMapper.listVideoBanner();
System.out.println("从数据库里面找轮播图列表");
return bannerList;
});
if(cacheObj instanceof List){
List<VideoBanner> bannerList = (List<VideoBanner>)cacheObj;
return bannerList;
}
}catch (Exception e){
e.printStackTrace();
}
return null;
# 视频列表引入本地缓存
播放列表加入本地缓存
@Override
public List<Video> listVideo() {
try{
Object cacheObj = baseCache.getTenMinuteCache().get(CacheKeyManager.INDEX_VIDEL_LIST,()->{
List<Video> videoList = videoMapper.listVideo();
return videoList;
});
if(cacheObj instanceof List){
List<Video> videoList = (List<Video>)cacheObj;
return videoList;
}
}catch (Exception e){
e.printStackTrace();
}
//可以返回兜底数据,业务系统降级-》SpringCloud专题课程
return null;
}
# 视频详情引入本地缓存
视频详情加入本地缓存
@Override
public Video findDetailById(int videoId) {
String videoCacheKey = String.format(CacheKeyManager.VIDEO_DETAIL,videoId);
try{
Object cacheObject = baseCache.getOneHourCache().get( videoCacheKey, ()->{
// 需要使用mybaits关联复杂查询
Video video = videoMapper.findDetailById(videoId);
return video;
});
if(cacheObject instanceof Video){
Video video = (Video)cacheObject;
return video;
}
}catch (Exception e){
e.printStackTrace();
}
return null;
}
# 实战接口压力测试, 明白优化前后的QPS并发差距和跨域配置
# 开启Guava缓存压测热点数据接口
简介: 启用缓存 压测热点数据接接口
-
视频轮播图接口 Throughput: 14000
-
注意:接口的性能影响因素很多:机器的配置如CPU、内存、当前负载情况等,还有网络带宽因素影响,只能尽量减少影响因素
单线程访问结果
{"code":0,"data":null,"msg":null}
# 取消Guava缓存压测热点数据接口和前后对比
简介: 不启用缓存 压测热点数据接口
- 视频轮播图接口 Throughput : 2700