实现openfire消息记录通常有两种方式,修改服务端和添加消息记录插件。

今天,简单的说明一下修改服务端方式实现消息记录保存功能。

实现思路

修改前:

默认的,openfire只提供保存离线记录至ofOffline表中。当发送一条消息时,判断用户是否在线,若为true,不保存消息;若为fasle,保存消息至离线消息表中。

修改后:

仿照保存离线消息,用户每发送一条消息,将消息存放在ofHistory表中,ofHistory表结构同ofOffline

实现步骤:

1.修改初始数据库文件,路径src/database/openfire_sqlserver.sql

添加ofHistory表

[sql] view plaincopyprint?

CREATE TABLE ofHistory (

username NVARCHAR(64) NOT NULL,

messageID INTEGER NOT NULL,

creationDate NVARCHAR(64) NOT NULL,

messageSize INTEGER NOT NULL,

stanza TEXT NOT NULL,

CONSTRAINT ofHistory_pk PRIMARY KEY (username, messageID)

);

CREATE TABLE ofOffline (

username NVARCHAR(64) NOT NULL,

messageID INTEGER NOT NULL,

creationDate CHAR(15) NOT NULL,

messageSize INTEGER NOT NULL,

stanza NTEXT NOT NULL,

CONSTRAINT ofOffline_pk PRIMARY KEY (username, messageID)

);

注:其他数据库修改方式雷同

2.添加保存消息方法

MessageRouter类中110行

[java] view plaincopyprint?

try {

// Deliver stanza to requested route

routingTable.routePacket(recipientJID, packet, false);

//保存消息记录dml@2013.4.15

OfflineMessageStore oms = new OfflineMessageStore();

oms.addMessage_toHistory(packet);

         }
catch (Exception e) {
log.error("Failed to route packet: " + packet.toXML(), e);
routingFailed(recipientJID, packet);
}

3.修改OfflineMessageStore类,添加保存消息记录方法

[java] view plaincopyprint?

/**

  • $RCSfile$
  • $Revision: 2911 $
  • $Date: 2005-10-03 12:35:52 -0300 (Mon, 03 Oct 2005) $
  • Copyright (C) 2004-2008 Jive Software. All rights reserved.
  • Licensed under the Apache License, Version 2.0 (the "License");
  • you may not use this file except in compliance with the License.
  • You may obtain a copy of the License at
  • http://www.apache.org/licenses/LICENSE-2.0
  • Unless required by applicable law or agreed to in writing, software
  • distributed under the License is distributed on an "AS IS" BASIS,
  • WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  • See the License for the specific language governing permissions and
  • limitations under the License.

    */

package org.jivesoftware.openfire;

import java.io.StringReader;

import java.sql.Connection;

import java.sql.PreparedStatement;

import java.sql.ResultSet;

import java.text.SimpleDateFormat;

import java.util.ArrayList;

import java.util.Collection;

import java.util.Date;

import java.util.List;

import java.util.Map;

import java.util.concurrent.BlockingQueue;

import java.util.concurrent.LinkedBlockingQueue;

import java.util.regex.Matcher;

import java.util.regex.Pattern;

import org.dom4j.DocumentException;

import org.dom4j.Element;

import org.dom4j.io.SAXReader;

import org.jivesoftware.database.DbConnectionManager;

import org.jivesoftware.database.SequenceManager;

import org.jivesoftware.openfire.container.BasicModule;

import org.jivesoftware.openfire.event.UserEventDispatcher;

import org.jivesoftware.openfire.event.UserEventListener;

import org.jivesoftware.openfire.user.User;

import org.jivesoftware.openfire.user.UserManager;

import org.jivesoftware.util.JiveConstants;

import org.jivesoftware.util.LocaleUtils;

import org.jivesoftware.util.StringUtils;

import org.jivesoftware.util.XMPPDateTimeFormat;

import org.jivesoftware.util.cache.Cache;

import org.jivesoftware.util.cache.CacheFactory;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.xmpp.packet.JID;

import org.xmpp.packet.Message;

/**

  • Represents the user's offline message storage. A message store holds messages

  • that were sent to the user while they were unavailable. The user can retrieve

  • their messages by setting their presence to "available". The messages will

  • then be delivered normally. Offline message storage is optional, in which

  • case a null implementation is returned that always throws

  • UnauthorizedException when adding messages to the store.

  • @author Iain Shigeoka

    */

    public class OfflineMessageStore extends BasicModule implements

    UserEventListener {

    private static final Logger Log = LoggerFactory

    .getLogger(OfflineMessageStore.class);

    // 保存消息记录 dml@2013.4.16

    private static final String INSERT_HISTORY = "INSERT INTO ofHistory (username, messageID, creationDate, messageSize, stanza) "

    + "VALUES (?, ?, ?, ?, ?)";

    private static final String INSERT_OFFLINE = "INSERT INTO ofOffline (username, messageID, creationDate, messageSize, stanza) "

    + "VALUES (?, ?, ?, ?, ?)";

    private static final String LOAD_OFFLINE = "SELECT stanza, creationDate FROM ofOffline WHERE username=?";

    private static final String LOAD_OFFLINE_MESSAGE = "SELECT stanza FROM ofOffline WHERE username=? AND creationDate=?";

    private static final String SELECT_SIZE_OFFLINE = "SELECT SUM(messageSize) FROM ofOffline WHERE username=?";

    private static final String SELECT_SIZE_ALL_OFFLINE = "SELECT SUM(messageSize) FROM ofOffline";

    private static final String DELETE_OFFLINE = "DELETE FROM ofOffline WHERE username=?";

    private static final String DELETE_OFFLINE_MESSAGE = "DELETE FROM ofOffline WHERE username=? AND creationDate=?";

    private static final int POOL_SIZE = 10;

    private Cache<String, Integer> sizeCache;

    /**

    • Pattern to use for detecting invalid XML characters. Invalid XML
    • characters will be removed from the stored offline messages.

      */

      private Pattern pattern = Pattern.compile("&\#[\d]+;");

    /**

    • Returns the instance of OfflineMessageStore being used by the
    • XMPPServer.
    • @return the instance of OfflineMessageStore being used by the
    •     XMPPServer.

    */

    public static OfflineMessageStore getInstance() {

    return XMPPServer.getInstance().getOfflineMessageStore();

    }

    /**

    • Pool of SAX Readers. SAXReader is not thread safe so we need to have a
    • pool of readers.

      */

      private BlockingQueue xmlReaders = new LinkedBlockingQueue(

      POOL_SIZE);

    /**

    • Constructs a new offline message store.

      */

      public OfflineMessageStore() {

      super("Offline Message Store");

      sizeCache = CacheFactory.createCache("Offline Message Size");

      }

    /**

    • Adds a message to this message store. Messages will be stored and made
    • available for later delivery.
    • @param message
    •        the message to store.

    */

    public void addMessage(Message message) {

    if (message == null) {

    return;

    }

    // ignore empty bodied message (typically chat-state notifications).

    if (message.getBody() == null || message.getBody().length() == 0) {

    // allow empty pubsub messages (OF-191)

    if (message.getChildElement("event",

    "http://jabber.org/protocol/pubsub#event") == null) {

    return;

    }

    }

    JID recipient = message.getTo();

    String username = recipient.getNode();

    // If the username is null (such as when an anonymous user), don't

    // store.

    if (username == null

    || !UserManager.getInstance().isRegisteredUser(recipient)) {

    return;

    } else if (!XMPPServer.getInstance().getServerInfo().getXMPPDomain()

    .equals(recipient.getDomain())) {

    // Do not store messages sent to users of remote servers

    return;

    }

     long messageID = SequenceManager.nextID(JiveConstants.OFFLINE);  
    
     // Get the message in XML format.
    String msgXML = message.getElement().asXML(); Connection con = null;
    PreparedStatement pstmt = null;
    try {
    con = DbConnectionManager.getConnection();
    pstmt = con.prepareStatement(INSERT_OFFLINE);
    pstmt.setString(1, username);
    pstmt.setLong(2, messageID);
    pstmt.setString(3, StringUtils.dateToMillis(new java.util.Date()));
    // SimpleDateFormat df = new
    // SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    // pstmt.setString(3, df.format(new Date()).toString()); pstmt.setInt(4, msgXML.length());
    pstmt.setString(5, msgXML);
    pstmt.executeUpdate();
    } catch (Exception e) {
    Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
    } finally {
    DbConnectionManager.closeConnection(pstmt, con);
    } // Update the cached size if it exists.
    if (sizeCache.containsKey(username)) {
    int size = sizeCache.get(username);
    size += msgXML.length();
    sizeCache.put(username, size);
    }

    }

    /**

    • 保存消息记录

    • @author dml

    • @param message

      */

      public void addMessage_toHistory(Message message) {

      if (message == null) {

      return;

      }

      // ignore empty bodied message (typically chat-state notifications).

      if (message.getBody() == null || message.getBody().length() == 0) {

      // allow empty pubsub messages (OF-191)

      if (message.getChildElement("event",

      "http://jabber.org/protocol/pubsub#event") == null) {

      return;

      }

      }

      JID recipient = message.getTo();

      String username = recipient.getNode();

      // If the username is null (such as when an anonymous user), don't

      // store.

      if (username == null

      || !UserManager.getInstance().isRegisteredUser(recipient)) {

      return;

      } else if (!XMPPServer.getInstance().getServerInfo().getXMPPDomain()

      .equals(recipient.getDomain())) {

      // Do not store messages sent to users of remote servers

      return;

      }

      long messageID = SequenceManager.nextID(JiveConstants.OFFLINE);

      // Get the message in XML format.

      String msgXML = message.getElement().asXML();

      Connection con = null;

      PreparedStatement pstmt = null;

      try {

      con = DbConnectionManager.getConnection();

      pstmt = con.prepareStatement(INSERT_HISTORY);

      pstmt.setString(1, username);

      pstmt.setLong(2, messageID);

      // pstmt.setString(3, StringUtils.dateToMillis(new

      // java.util.Date()));

      SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

      pstmt.setString(3, df.format(new Date()).toString());

       pstmt.setInt(4, msgXML.length());
      pstmt.setString(5, msgXML);
      pstmt.executeUpdate();

      }

      catch (Exception e) {

      Log.error(LocaleUtils.getLocalizedString("admin.error"), e);

      } finally {

      DbConnectionManager.closeConnection(pstmt, con);

      }

      // Update the cached size if it exists.

      if (sizeCache.containsKey(username)) {

      int size = sizeCache.get(username);

      size += msgXML.length();

      sizeCache.put(username, size);

      }

      }

    /**

    • Returns a Collection of all messages in the store for a user. Messages

    • may be deleted after being selected from the database depending on the

    • delete param.

    • @param username

    •        the username of the user who's messages you'd like to receive.
    • @param delete

    •        true if the offline messages should be deleted.
    • @return An iterator of packets containing all offline messages.

      */

      public Collection getMessages(String username,

      boolean delete) {

      List messages = new ArrayList();

      SAXReader xmlReader = null;

      Connection con = null;

      PreparedStatement pstmt = null;

      ResultSet rs = null;

      try {

      // Get a sax reader from the pool

      xmlReader = xmlReaders.take();

      con = DbConnectionManager.getConnection();

      pstmt = con.prepareStatement(LOAD_OFFLINE);

      pstmt.setString(1, username);

      rs = pstmt.executeQuery();

      while (rs.next()) {

      String msgXML = rs.getString(1);

      // 解析时间eg.Tue Apr 16 15:32:39 CST 2013

      Date creationDate = new Date(Long.parseLong(rs.getString(2)

      .trim()));

      OfflineMessage message;

      try {

      message = new OfflineMessage(creationDate, xmlReader.read(

      new StringReader(msgXML)).getRootElement());

      } catch (DocumentException e) {

      // Try again after removing invalid XML chars (e.g. )

      Matcher matcher = pattern.matcher(msgXML);

      if (matcher.find()) {

      msgXML = matcher.replaceAll("");

      }

      message = new OfflineMessage(creationDate, xmlReader.read(

      new StringReader(msgXML)).getRootElement());

      }

           // Add a delayed delivery (XEP-0203) element to the message.
      Element delay = message.addChildElement("delay",
      "urn:xmpp:delay");
      delay.addAttribute("from", XMPPServer.getInstance()
      .getServerInfo().getXMPPDomain());
      delay.addAttribute("stamp",
      XMPPDateTimeFormat.format(creationDate));
      // Add a legacy delayed delivery (XEP-0091) element to the
      // message. XEP is obsolete and support should be dropped in
      // future.
      delay = message.addChildElement("x", "jabber:x:delay");
      delay.addAttribute("from", XMPPServer.getInstance()
      .getServerInfo().getXMPPDomain());
      delay.addAttribute("stamp",
      XMPPDateTimeFormat.formatOld(creationDate));
      messages.add(message);
      }
      // Check if the offline messages loaded should be deleted, and that
      // there are
      // messages to delete.
      if (delete && !messages.isEmpty()) {
      PreparedStatement pstmt2 = null;
      try {
      pstmt2 = con.prepareStatement(DELETE_OFFLINE);
      pstmt2.setString(1, username);
      pstmt2.executeUpdate();
      removeUsernameFromSizeCache(username);
      } catch (Exception e) {
      Log.error("Error deleting offline messages of username: "
      + username, e);
      } finally {
      DbConnectionManager.closeStatement(pstmt2);
      }
      }

      } catch (Exception e) {

      Log.error("Error retrieving offline messages of username: "

      + username, e);

      } finally {

      DbConnectionManager.closeConnection(rs, pstmt, con);

      // Return the sax reader to the pool

      if (xmlReader != null) {

      xmlReaders.add(xmlReader);

      }

      }

      return messages;

      }

    /**

    • Returns the offline message of the specified user with the given creation
    • date. The returned message will NOT be deleted from the database.
    • @param username
    •        the username of the user who's message you'd like to receive.
    • @param creationDate
    •        the date when the offline message was stored in the database.
    • @return the offline message of the specified user with the given creation
    •     stamp.

    */

    public OfflineMessage getMessage(String username, Date creationDate) {

    OfflineMessage message = null;

    Connection con = null;

    PreparedStatement pstmt = null;

    ResultSet rs = null;

    SAXReader xmlReader = null;

    try {

    // Get a sax reader from the pool

    xmlReader = xmlReaders.take();

    con = DbConnectionManager.getConnection();

    pstmt = con.prepareStatement(LOAD_OFFLINE_MESSAGE);

    pstmt.setString(1, username);

    pstmt.setString(2, StringUtils.dateToMillis(creationDate));

    rs = pstmt.executeQuery();

    while (rs.next()) {

    String msgXML = rs.getString(1);

    message = new OfflineMessage(creationDate, xmlReader.read(

    new StringReader(msgXML)).getRootElement());

    // Add a delayed delivery (XEP-0203) element to the message.

    Element delay = message.addChildElement("delay",

    "urn:xmpp:delay");

    delay.addAttribute("from", XMPPServer.getInstance()

    .getServerInfo().getXMPPDomain());

    delay.addAttribute("stamp",

    XMPPDateTimeFormat.format(creationDate));

    // Add a legacy delayed delivery (XEP-0091) element to the

    // message. XEP is obsolete and support should be dropped in

    // future.

    delay = message.addChildElement("x", "jabber❌delay");

    delay.addAttribute("from", XMPPServer.getInstance()

    .getServerInfo().getXMPPDomain());

    delay.addAttribute("stamp",

    XMPPDateTimeFormat.formatOld(creationDate));

    }

    } catch (Exception e) {

    Log.error("Error retrieving offline messages of username: "

    + username + " creationDate: " + creationDate, e);

    } finally {

    // Return the sax reader to the pool

    if (xmlReader != null) {

    xmlReaders.add(xmlReader);

    }

    DbConnectionManager.closeConnection(rs, pstmt, con);

    }

    return message;

    }

    /**

    • Deletes all offline messages in the store for a user.
    • @param username
    •        the username of the user who's messages are going to be
    •        deleted.

    */

    public void deleteMessages(String username) {

    Connection con = null;

    PreparedStatement pstmt = null;

    try {

    con = DbConnectionManager.getConnection();

    pstmt = con.prepareStatement(DELETE_OFFLINE);

    pstmt.setString(1, username);

    pstmt.executeUpdate();

         removeUsernameFromSizeCache(username);
    } catch (Exception e) {
    Log.error("Error deleting offline messages of username: "
    + username, e);
    } finally {
    DbConnectionManager.closeConnection(pstmt, con);
    }

    }

    private void removeUsernameFromSizeCache(String username) {

    // Update the cached size if it exists.

    if (sizeCache.containsKey(username)) {

    sizeCache.remove(username);

    }

    }

    /**

    • Deletes the specified offline message in the store for a user. The way to
    • identify the message to delete is based on the creationDate and username.
    • @param username
    •        the username of the user who's message is going to be deleted.
    • @param creationDate
    •        the date when the offline message was stored in the database.

    */

    public void deleteMessage(String username, Date creationDate) {

    Connection con = null;

    PreparedStatement pstmt = null;

    try {

    con = DbConnectionManager.getConnection();

    pstmt = con.prepareStatement(DELETE_OFFLINE_MESSAGE);

    pstmt.setString(1, username);

    pstmt.setString(2, StringUtils.dateToMillis(creationDate));

    pstmt.executeUpdate();

         // Force a refresh for next call to getSize(username),
    // it's easier than loading the message to be deleted just
    // to update the cache.
    removeUsernameFromSizeCache(username);
    } catch (Exception e) {
    Log.error("Error deleting offline messages of username: "
    + username + " creationDate: " + creationDate, e);
    } finally {
    DbConnectionManager.closeConnection(pstmt, con);
    }

    }

    /**

    • Returns the approximate size (in bytes) of the XML messages stored for a
    • particular user.
    • @param username
    •        the username of the user.
    • @return the approximate size of stored messages (in bytes).

      */

      public int getSize(String username) {

      // See if the size is cached.

      if (sizeCache.containsKey(username)) {

      return sizeCache.get(username);

      }

      int size = 0;

      Connection con = null;

      PreparedStatement pstmt = null;

      ResultSet rs = null;

      try {

      con = DbConnectionManager.getConnection();

      pstmt = con.prepareStatement(SELECT_SIZE_OFFLINE);

      pstmt.setString(1, username);

      rs = pstmt.executeQuery();

      if (rs.next()) {

      size = rs.getInt(1);

      }

      // Add the value to cache.

      sizeCache.put(username, size);

      } catch (Exception e) {

      Log.error(LocaleUtils.getLocalizedString("admin.error"), e);

      } finally {

      DbConnectionManager.closeConnection(rs, pstmt, con);

      }

      return size;

      }

    /**

    • Returns the approximate size (in bytes) of the XML messages stored for
    • all users.
    • @return the approximate size of all stored messages (in bytes).

      */

      public int getSize() {

      int size = 0;

      Connection con = null;

      PreparedStatement pstmt = null;

      ResultSet rs = null;

      try {

      con = DbConnectionManager.getConnection();

      pstmt = con.prepareStatement(SELECT_SIZE_ALL_OFFLINE);

      rs = pstmt.executeQuery();

      if (rs.next()) {

      size = rs.getInt(1);

      }

      } catch (Exception e) {

      Log.error(LocaleUtils.getLocalizedString("admin.error"), e);

      } finally {

      DbConnectionManager.closeConnection(rs, pstmt, con);

      }

      return size;

      }

    public void userCreated(User user, Map params) {

    // Do nothing

    }

    public void userDeleting(User user, Map params) {

    // Delete all offline messages of the user

    deleteMessages(user.getUsername());

    }

    public void userModified(User user, Map params) {

    // Do nothing

    }

    @Override

    public void start() throws IllegalStateException {

    super.start();

    // Initialize the pool of sax readers

    for (int i = 0; i < POOL_SIZE; i++) {

    SAXReader xmlReader = new SAXReader();

    xmlReader.setEncoding("UTF-8");

    xmlReaders.add(xmlReader);

    }

    // Add this module as a user event listener so we can delete

    // all offline messages when a user is deleted

    UserEventDispatcher.addListener(this);

    }

    @Override

    public void stop() {

    super.stop();

    // Clean up the pool of sax readers

    xmlReaders.clear();

    // Remove this module as a user event listener

    UserEventDispatcher.removeListener(this);

    }

    }

源码详解openfire保存消息记录_修改服务端方式的更多相关文章

  1. RocketMQ源码详解 | Producer篇 · 其二:消息组成、发送链路

    概述 在上一节 RocketMQ源码详解 | Producer篇 · 其一:Start,然后 Send 一条消息 中,我们了解了 Producer 在发送消息的流程.这次我们再来具体下看消息的构成与其 ...

  2. RocketMQ源码详解 | Consumer篇 · 其一:消息的 Pull 和 Push

    概述 当消息被存储后,消费者就会将其消费. 这句话简要的概述了一条消息的最总去向,也引出了本文将讨论的问题: 消息什么时候才对被消费者可见? 是在 page cache 中吗?还是在落盘后?还是像 K ...

  3. RocketMQ源码详解 | Broker篇 · 其四:事务消息、批量消息、延迟消息

    概述 在上文中,我们讨论了消费者对于消息拉取的实现,对于 RocketMQ 这个黑盒的心脏部分,我们顺着消息的发送流程已经将其剖析了大半部分.本章我们不妨乘胜追击,接着讨论各种不同的消息的原理与实现. ...

  4. 源码详解系列(七) ------ 全面讲解logback的使用和源码

    什么是logback logback 用于日志记录,可以将日志输出到控制台.文件.数据库和邮件等,相比其它所有的日志系统,logback 更快并且更小,包含了许多独特并且有用的特性. logback ...

  5. Activiti架构分析及源码详解

    目录 Activiti架构分析及源码详解 引言 一.Activiti设计解析-架构&领域模型 1.1 架构 1.2 领域模型 二.Activiti设计解析-PVM执行树 2.1 核心理念 2. ...

  6. 源码详解系列(六) ------ 全面讲解druid的使用和源码

    简介 druid是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能,另外,druid还扩展 ...

  7. RocketMQ源码详解 | Broker篇 · 其三:CommitLog、索引、消费队列

    概述 上一章中,已经介绍了 Broker 的文件系统的各个层次与部分细节,本章将继续了解在逻辑存储层的三个文件 CommitLog.IndexFile.ConsumerQueue 的一些细节.文章最后 ...

  8. RocketMQ源码详解 | Broker篇 · 其五:高可用之主从架构

    概述 对于一个消息中间件来讲,高可用功能是极其重要的,RocketMQ 当然也具有其对应的高可用方案. 在 RocketMQ 中,有主从架构和 Dledger 两种高可用方案: 第一种通过主 Brok ...

  9. Spark Streaming揭秘 Day25 StreamingContext和JobScheduler启动源码详解

    Spark Streaming揭秘 Day25 StreamingContext和JobScheduler启动源码详解 今天主要理一下StreamingContext的启动过程,其中最为重要的就是Jo ...

随机推荐

  1. 使用 CSS3 实现 3D 图片滑块效果【附源码下载】

    使用 CSS3 的3D变换特性,我们可以通过让元素在三维空间中变换来实现一些新奇的效果. 这篇文章分享的这款 jQuery 立体图片滑块插件,利用了 3D transforms(变换)属性来实现多种不 ...

  2. Javascript: 从prototype漫谈到继承(2)

    本文同时也发表在我另一篇独立博客 <Javascript: 从prototype漫谈到继承(2)>(管理员请注意!这两个都是我自己的原创博客!不要踢出首页!不是转载!已经误会三次了!) 上 ...

  3. Android学习笔记之Fast Json的使用

    PS:最近这两天发现了Fast Json 感觉实在是强大.. 学习内容: 1.什么是Fast Json 2.如何使用Fast Json 3.Fast Json的相关原理 4.Fast Json的优势, ...

  4. 语义化HTML:p、h1-6、q、blockquote、hr、address、code、pre、var、cite、dfn和samp

    一.元素语义 p标签 W3C草案: The p element represents a paragraph.W3C specification 语义化的 <p>元素 表示:文章中的段落. ...

  5. OpenJudge 2990:符号三角形 解析报告

    2990:符号三角形 总时间限制:  1000ms       内存限制:  65536kB 描述 符号三角形的第1行有n个由“+”和”-“组成的符号 ,以后每行符号比上行少1个,2个同号下面是”+“ ...

  6. iOS开发的一些奇巧淫技

    TableView不显示没内容的Cell怎么办? 类似这种,我不想让下面那些空的显示. 很简单. self.tableView.tableFooterView = [[UIView alloc] in ...

  7. Android Studio快捷键每日一练(2)

    原文地址:http://www.developerphil.com/android-studio-tips-of-the-day-roundup-2/ 12.复制行 苹果:Cmd+D    Windo ...

  8. C语言学习013:通过make编译C源代码

    编译多个C源代码文件 当程序文件越来越多,修改了其中的一部分代码文件,我们并不需要全部重新编译,只需要编译其中一部分就可以,下面我们创建了一个launch程序,除了主程序,我们创建了3个功能代码文件r ...

  9. NetworkError: 404 Not Found - http://www.companyName.com/Content/fonts/ubuntu-regular-webfont.woff2

    网站是使用BootStrap框架实现,当站点发布至服务器(Windows Server 2008 R2)IIS之后,显示下面的异常: Insus.NET跑至相关目录之下检查,这些字体的文件是确实存在的 ...

  10. PHP访问MySql数据库介绍

    在网站后台,经常要与数据库打交道.本文介绍如何使用XAMPP来管理MySql数据库及如何用PHP来访问MySql数据库. 一.使用XAMPP来管理MySql数据库 首先使用XAMPP打开MySql的管 ...