javaweb:在线聊天网站(框架版)

2017-02-16 19:51

之前写过一次在线聊天网站,不过那次是无框架版的,这次用框架构建网站,基本功能和上次差不多

代码:

github: https://github.com/ZhongWenhui1995/javaweb-spring-hibernate-

涉及知识

java

spring(4.3.5):spring、spring MVC

hibernate

bootstrap

jsp

JavaScript,jquery

websocket

mysql

功能

1.用户的登录、注册、注销、密码修改

2.获知在线用户名字及数量

3.向在线用户发送消息

4.查看与该用户的历史信息

5.当有非当前聊天用户的信息到来时,会有提示

数据库

类一览

javaweb:在线聊天网站(框架版)0

mysql数据库建立

一个账户表,一个聊天内容表

create database db_talk;
create table tbl_account
(
    id int not null primary key auto_increment,
    name varchar(30) not null unique,
    password char(20) not null
)CHARACTER SET 'utf8'
COLLATE 'utf8_general_ci';

create table tbl_talk
(
    id int not null primary key auto_increment,
    content varchar(255),
    srcAccountId int not null,
    targetAccountId int not null,
    time datetime not null default now(),
    foreign key(srcAccountId) references tbl_account(id),
    foreign key(targetAccountId) references tbl_account(id)
)CHARACTER SET 'utf8'
COLLATE 'utf8_general_ci';

hibernate实体层bean

P.S.关于hibernate注解解释可看 http://blog.csdn.net/name_z/article/details/51318271 实体类Account和TalkContent都继承了通用实体类CommonBean

CommonBean

@MappedSuperclass
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class CommonBean {
    @Id
    @Column(name = "id", insertable = false, updatable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected int id;
    @Transient
    protected static final String DEFAULT_ID_COLUMN_NAME = "id";

    public int getId() {
        return id;
    }
//    public void setId(int id) {
//        this.id = id;
//    }

    //获取本类的简单类名
    public abstract String getClassSimpleName();
    public String getIdColumnName(){
        return DEFAULT_ID_COLUMN_NAME;
    }
}

@Entity
@Table(name = "tbl_talk")
public class TalkContent extends CommonBean {...}

@Entity
@Table(name = "tbl_account")
public class Account extends CommonBean {...}

使用CommonBean原因

1.各实体类的相同部分(如这次的id)可放在CommonBean中,从而减少了代码量,使代码整洁

2.采用abstract方法约束子对象,方便后面dao层使通用持久层CommonDao类

hibernate持久层dao

使用模板与反射减少代码量。

所有dao类接口都继承了ICommonDao,CommonDao实现了ICommonDao,所有dao类实现都继承了CommonDao

CommonDao使用模板设计,所有子类能不做修改直接调用CommonDao中方法进行实体的增删改查

接口

public interface ICommonDao<T extends CommonBean> {
    boolean save(T entity);
    boolean delete(T entity);
    boolean updateByID(T entity);
    T getByID(T entity);
    T getByID(int id);
    List<T> getAll();
    void setSessionFactory(SessionFactory sessionFactory);
}

public interface IAccountDao extends ICommonDao<Account> {
    Account getByName(String name);
    Account getByName(Account account);
}

public interface ITalkContentDao extends ICommonDao<TalkContent> {...}

CommonBean

通过继承实现CommonBean类后,对于增删改查的方法均可不作修改直接调用父类CommonBean的增删改查即可对子类进行操作

@Repository
public abstract class CommonDao<T extends CommonBean> implements ICommonDao<T> {

    @Autowired
    protected SessionFactory sessionFactory;
    private static final String SAVE_METHOD = "save";
    private static final String DELETE_METHOD = "delete";
    private static final String UPDATE_METHOD = "update";
    protected final String CLASS_SIMPLENAME;
    protected final String ID_COLUMN_NAME;

    /**
     * 传入一个已经实体化的对象(非null对象),并用该对象初始化CLASS_SIMPLENAME、ID_COLUMN_NAME两个变量(CommonBean中有这两个方法)
     * 
     * @param entity
     */
    public CommonDao(T entity) {
        this.CLASS_SIMPLENAME = entity.getClassSimpleName();
        this.ID_COLUMN_NAME = entity.getIdColumnName();
    }

    /**
     * 获取开启事务后的session
     * 
     * @return
     */
    protected Session getCurrentSession() {
        Session session = this.sessionFactory.getCurrentSession();
        try {
            session.beginTransaction();
        } catch (TransactionException e) {

        }
        return session;
    }

    /**
     * 传入hql语句进行查询,返回list
     * 
     * @param hql
     * @return
     */
    @SuppressWarnings("unchecked")
    protected List<T> queryList(String hql) {
        Session session = this.getCurrentSession();
        Query query = session.createQuery(hql);
        return query.list();
    }

    /**
     * 传入hql语句进行查询,如果查询不到结果,返回null
     * 
     * @param hql
     * @return
     */
    @SuppressWarnings("unchecked")
    protected T queryUnique(String hql) {
        Session session = this.getCurrentSession();
        Query query = session.createQuery(hql);
        return (T) query.uniqueResult();
    }

    @Override
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    private Method getMethod(String operation) throws Exception {
        return Session.class.getMethod(operation, Object.class);
    }

    /**
     * 通过反射执行执行增、删、改操作
     * 
     * @param oper
     * @param entity
     * @return
     */
    private boolean execute(String oper, T entity) {
        try {
            Session session = this.getCurrentSession();
            Method method = this.getMethod(oper);
            method.invoke(session, entity);
            session.getTransaction().commit();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    @Override
    public boolean save(T entity) {
        return this.execute(SAVE_METHOD, entity);
    }

    @Override
    public boolean delete(T entity) {
        return this.execute(DELETE_METHOD, entity);
    }

    @Override
    public boolean updateByID(T entity) {
        return this.execute(UPDATE_METHOD, entity);
    }

    @Override
    public T getByID(T entity) {
        return this.getByID(entity.getId());
    }

    @Override
    public T getByID(int id) {
        return this.queryUnique(HQLGenerator.generateSingleEqualQueryHql(this.CLASS_SIMPLENAME,
        this.ID_COLUMN_NAME, String.valueOf(id)));
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<T> getAll() {
        Session session = this.getCurrentSession();
        return session.createQuery(HQLGenerator.generateAllQuery(this.CLASS_SIMPLENAME)).list();
    }

AccountDao类:

@Repository
public class AccountDao extends CommonDao<Account> implements IAccountDao {

    private static final String NAME_PROP_NAME = "name";

    public AccountDao() {
        super(new Account());
    }

    @Override
    public Account getByName(String name) {
        return this.queryUnique(HQLGenerator.generateSingleEqualQueryHql(this.CLASS_SIMPLENAME, NAME_PROP_NAME, name));
    }

    @Override
    public Account getByName(Account account) {
        return this.getByName(account.getName());
    }

}

hql语句生成

hql语句的获取统一从一个类中获取,负责的hql语句直接写成static final变量

public class HQLGenerator {

        //from classname obj where obj.column = 'value'
        private static final String SINGLE_QUERY = ALL_QUERY + " obj where obj." + COLUMN_PLACER + BLANK + EQUAL + BLANK + SINGLE_QUOTE + VALUE_PLACER + SINGLE_QUOTE;
        ...
        /**
         * 生成对单个列的值进行查询的hql语句
         * 
         * @param classname
         *            类名
         * @param column
         *            列名
         * @param value
         *            值
         * @return
         */
        public static String generateSingleEqualQueryHql(String classname, String column, String value) {
            return SINGLE_QUERY.replace(CLASSNAME_PLACER, classname).replace(COLUMN_PLACER, column).replace(VALUE_PLACER,
            value);
        }
        ...
}

网页交互

类一览:

javaweb:在线聊天网站(框架版)1

AccountManager:负责账户的登录、修改

OnlineAccountManager:保存当前在线账户的名字与session联系

TalkManager:负责聊天内容的保存,分为保存到内存和保存到数据库

TalkManager

可以指定保存在内存中的聊天内容的最大数量,超过清空

保存到内存中的聊天内容

每次点开目标对象,都会显示这些内容(服务器中途没有关闭过)

保存到数据库中的内容

只保存在数据库中,没有保存在内存中的内容,只有点击历史信息后,才能显示,点击历史信息后,这部分内容也会保存到内存中

spring mvc表单提交

就是点击按钮后,客户端才能发送数据给服务器,服务器spring mvc返回视图以及数据

登录页面jsp:

<form:form method="POST"  modelAttribute="account"  action="/Talk/login.do">
    <table>
        <tr>
            <td><form:label path="name" >名字:</form:label></td>
            <td><form:input path="name" id="name" class="form-control input-sm"/></td>
        </tr>
        <tr>
            <td><form:label path="password">密码:</form:label></td>
            <td><form:password path="password" id="password" class="form-control input-sm"/></td>
        </tr>
        <tr>
            <td colspan="2"><input type="submit" value="登录" /></td>
        </tr>
    </table>
</form:form>

login的controller:

@Controller
public class LoginController {

    @RequestMapping(method = RequestMethod.POST, path = "/login")
    public String login(ModelMap model, Account account) {
        account = AccountManager.login(account);
        if (account != null) {
            TalkContent talkContent = new TalkContent();
            talkContent.setSrcAccount(account);
            talkContent.setTargetAccountName("");
            //给视图添加数据
            model.addAttribute("talkContent", talkContent);
            model.addAttribute("onlineNum", OnlineAccountManager.getOnlineNum());
            model.addAttribute("accounts", OnlineAccountManager.getOnlineAccountsName());
            return "main";
        }
        model.addAttribute("warnMessage", AccountManager.ERROR_LOGIN);
        return "login";
    }

    @RequestMapping(method = RequestMethod.GET, path = "/login")
    public String getLoginJsp(ModelMap model) {
        //因为jsp页面提交中注明account,因此必须添加account属性
        model.addAttribute("account", new Account());
        model.addAttribute("warnMessage", "");
        return "login";
    }
}

使用websocket和jquery完成实时数据显示

因为客户端与客户端之间的交互需要实时性(发送的、接收到的信息能马上显示),因此不能用spring mvc表单提交,要用websocket。而使用websocket提交信息后,服务器只会发回信息,而不是网页,因此需要jquery实时更新当前显示的信息,包括聊天信息和提示信息

步骤:

1.客户端与服务器建立联系后,客户端马上向服务器发送注册信息(将账户名字与(websocket)session构成联系),每次的刷新都回导致注册信息的更新

2.用户点击发送按钮后,客户端向服务器发送信息,服务器保存信息并且发送给目标用户

3.用户关闭页面时,服务器获知并且在注册信息表中删除该用户

main.jsp

注意点:

聊天内容和提示信息除了有id方便jquery的操作,还需要 ${}保证 当点击刷新或者其他spring mvc提交表单的操作后, 聊天内容和提示信息可以从服务器中获取而显示 ,否则,只有jquery设置的内容会丢失。

...
<!-- 聊天内容部分 -->
<div class="panel-body" data-spy="scroll" data-target="#navbar-example" data-offset="300" style="height: 300px; overflow: auto; position: relative;">
    <pre id="talks">${talks}</pre>
</div>
...
<!-- 信息提示部分 -->
<div class="panel-body" data-spy="scroll" data-target="#navbar-example" data-offset="0" style="height: 250px; overflow: auto; position: relative;">
    <pre id="message">${message}</pre>
</div>
...
<!-- 发送聊天内容部分 -->
<div class="input-group">
    <input id="talk" type="text" class="form-control"> 
    <span class="input-group-addon"> <button id="send" type="button" class="btn btn-default">发送</button></span>
</div>
...

main.js

//发送聊天内容,格式为:from(发送人)to(接收人)talk(聊天内容)
function setBtnSend() {
    $(document).ready(
            function() {
                $("#send").click(
                        function() {
                            var srcName = $("#accountName").text();
                            var targetName = $("#targetAccountName").text();
                            var talk = $("#talk").val();
                            var message = "from(" + srcName + ")to(" + targetName + ")talk(" + talk + ")";
                            sendMessage(message);
                            var temp = $("#talks").text();
                            $("#talks").text(temp + "\n" + srcName + ">" + talk);
                        });
            });

}

function setWebSocket() {
    //当与服务器建立联系后,马上发送注册信息,因此每次页面的刷新也会重新发送注册信息
    webSocket.onopen = function(event) {
        var message = "regist(" + $("#accountName").text() + ")";
        sendMessage(message);
    };
    ...
    //接受到服务器发送的信息
    webSocket.onmessage = function(event) {
        var targetName = $("#targetAccountName").text();
        var temp = $("#talks").text();
        var message = event.data;
        //判断发送对象是否为当前聊天对象,是则显示到聊天内容,否则显示到提示信息中
        if (message.indexOf(targetName + ">") > 0) {
            $("#talks").text(temp + "\n" + message);
        } else {
            $("#message").text(message);
        }
    }
}

服务器负责接受websocket的controller

@Controller
@ServerEndpoint("/talk")
public class TalkWebSocket extends TextWebSocketHandler {
    ...
    /**
     * 接受客户端发送的信息,如果为注册信息则注册(将用户名字与该session建立联系),否则为聊天信息,如果为有效聊天信息则保存到数据库,并且发送给目标账户
     * @param message
     * @param session
     * @throws IOException
     */
    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        message = URLDecoder.decode(message, "UTF-8");
        if(message.startsWith(REGIST)){
            //注册该用户,将名字与session构成联系
                OnlineAccountManager.regist(regexGroupOne(REGIST_PATTERN, message), session);
        }else{
            String srcAccountName = regexGroupOne(SRCACCOUNT_PATTERN, message);
            String targetAccountName = regexGroupOne(TARGETACCOUNT_PATTERN, message);
            String talk = regexGroupOne(TALK_PATTERN, message);
            //如果接收人为空或者聊天内容为空,直接忽略该条信息
            if(targetAccountName == null || "".equals(talk)){
                return ;
            }
            //根据名字获取session
            Session targetSession = OnlineAccountManager.getSession(targetAccountName);
            if(targetSession != null){
                //保存聊天内容到数据库
                TalkManager.saveTalkInDatabase(srcAccountName, targetAccountName, talk);
                //使用session向客户端发送信息
                targetSession.getBasicRemote().sendText(srcAccountName + ">" + talk);
            }
        }
    }
}

bootstrap

采用网格系统作为基础构建网页,使用部分bootstrap中的部件:导航栏、输入狂组、按钮、面板

效果展示

登录页面

注册页面

主页:

刚登录进来的页面:

javaweb:在线聊天网站(框架版)6 javaweb:在线聊天网站(框架版)7

发送消息:

javaweb:在线聊天网站(框架版)8

接受信息但当前聊天内容并不是该发送者:

javaweb:在线聊天网站(框架版)9

接受信息:

javaweb:在线聊天网站(框架版)10