zoukankan      html  css  js  c++  java
  • ActiveMQ使用线程池实现消息的生产与消费

    jar文件:spring3.1jar,以及

    项目src路径下文件:config.properties

    读取config.properties文件JAVA类:

    package com.lejob.lejobmy.config;

    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.Properties;

    public class Configuration
    {
    private Properties propertie;
    private FileOutputStream outputFile;

    /**
    * 初始化Configuration类
    */
    public Configuration()
    {
    propertie = new Properties();
    }

    /**
    * 初始化Configuration类
    * @param filePath 要读取的配置文件的路径+名称
    */
    public Configuration(String filePath)
    {
    propertie = new Properties();
    try {
    InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(filePath);
    propertie.load(inputStream);
    inputStream.close();
    } catch (FileNotFoundException ex) {
    System.out.println("读取属性文件--->失败!- 原因:文件路径错误或者文件不存在");
    ex.printStackTrace();
    } catch (IOException ex) {
    System.out.println("装载文件--->失败!");
    ex.printStackTrace();
    }
    }//end ReadConfigInfo(...)

    /**
    * 重载函数,得到key的值
    * @param key 取得其值的键
    * @return key的值
    */
    public String getValue(String key)
    {
    if(propertie.containsKey(key)){
    String value = propertie.getProperty(key);//得到某一属性的值
    return value;
    }
    else
    return "";
    }//end getValue(...)

    /**
    * 重载函数,得到key的值
    * @param fileName properties文件的路径+文件名
    * @param key 取得其值的键
    * @return key的值
    */
    public String getValue(String fileName, String key)
    {
    try {
    String value = "";
    InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(fileName);
    propertie.load(inputStream);
    inputStream.close();
    if(propertie.containsKey(key)){
    value = propertie.getProperty(key);
    return value;
    }else
    return value;
    } catch (FileNotFoundException e) {
    e.printStackTrace();
    return "";
    } catch (IOException e) {
    e.printStackTrace();
    return "";
    } catch (Exception ex) {
    ex.printStackTrace();
    return "";
    }
    }//end getValue(...)

    /**
    * 清除properties文件中所有的key和其值
    */
    public void clear()
    {
    propertie.clear();
    }//end clear();

    /**
    * 改变或添加一个key的值,当key存在于properties文件中时该key的值被value所代替,
    * 当key不存在时,该key的值是value
    * @param key 要存入的键
    * @param value 要存入的值
    */
    public void setValue(String key, String value)
    {
    propertie.setProperty(key, value);
    }//end setValue(...)

    /**
    * 将更改后的文件数据存入指定的文件中,该文件可以事先不存在。
    * @param fileName 文件路径+文件名称
    * @param description 对该文件的描述
    */
    public void saveFile(String fileName, String description)
    {
    try {
    outputFile = new FileOutputStream(fileName);
    propertie.store(outputFile, description);
    outputFile.close();
    } catch (FileNotFoundException e) {
    e.printStackTrace();
    } catch (IOException ioe){
    ioe.printStackTrace();
    }
    }//end saveFile(...)

    public static void main(String[] args)
    {
    Configuration rc = new Configuration("config.properties");//相对路径
    String ip = rc.getValue("activemq_url");//以下读取properties文件的值
    String host = rc.getValue("activemq_user");
    String tab = rc.getValue("activemq_pw");
    //以下输出properties读出的值
    System.out.println("ip = " + ip);
    System.out.println("host = " + host);
    System.out.println("tab = " + tab);

    // Configuration cf = new Configuration();
    // String ipp = cf.getValue("./config/test.properties", "ip");
    // System.out.println("ipp = " + ipp);
    // cf.clear();
    // cf.setValue("min", "999");
    // cf.setValue("max", "1000");
    // cf.saveFile("./config/save.perperties", "test");

    // Configuration saveCf = new Configuration();
    // saveCf.setValue("min", "10");
    // saveCf.setValue("max", "1000");
    // saveCf.saveFile("./config/save.perperties");

    }

    }

    ActiveMQ消息生产者工厂类ActivemqConnectionFactory.java:

    package com.lejob.lejobsearch.index.queue;

    import java.io.Serializable;
    import java.util.Enumeration;
    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;

    import javax.jms.Connection;
    import javax.jms.DeliveryMode;
    import javax.jms.Destination;
    import javax.jms.JMSException;
    import javax.jms.MapMessage;
    import javax.jms.Message;
    import javax.jms.MessageConsumer;
    import javax.jms.MessageProducer;
    import javax.jms.ObjectMessage;
    import javax.jms.Session;
    import javax.jms.TextMessage;

    import org.apache.activemq.ActiveMQConnectionFactory;
    import org.apache.activemq.pool.PooledConnectionFactory;
    import org.apache.log4j.Logger;

    import com.lejob.rpc.search.model.QueueModel;


    public class ActivemqConnectionFactory {
    private static Logger logger = Logger.getLogger(ActivemqConnectionFactory.class);


    //设置连接的最大连接数
    private static int maxConnections = 5;
    //设置每个连接中使用的最大活动会话数
    private static int maximumActiveSessionPerConnection = 300;
    //线程池数量
    private static int threadPoolSize = 50;
    //强制使用同步返回数据的格式
    private static boolean useAsyncSendForJMS = true;

    private static ExecutorService threadPool;

    private static PooledConnectionFactory connectionFactory;


    public void init(){
    try {
    //设置JAVA线程池
    threadPool = Executors.newFixedThreadPool(threadPoolSize);
    //ActiveMQ的连接工厂
    ActiveMQConnectionFactory actualConnectionFactory = new ActiveMQConnectionFactory(ActiveMQConstant.ACTIVEMQ_USER_NAME, ActiveMQConstant.ACTIVEMQ_USER_PW, ActiveMQConstant.ACTIVEMQ_URL);
    actualConnectionFactory.setUseAsyncSend(useAsyncSendForJMS);
    //Active中的连接池工厂
    connectionFactory = new PooledConnectionFactory(actualConnectionFactory);
    connectionFactory.setCreateConnectionOnStartup(true);
    connectionFactory.setMaxConnections(maxConnections);
    connectionFactory.setMaximumActiveSessionPerConnection(maximumActiveSessionPerConnection);
    logger.info("INIT ACTIVEMQ POOL CONNECTION FACTORY SUCCESS......");
    } catch (Exception e) {
    logger.error("ACTIVEMQ CONNECTION INIT ERROR......", e);
    }
    }
    public void destroy(){
    try {
    if(connectionFactory != null){
    connectionFactory.stop();
    logger.info("STOP ACTIVEMQ CONNECTION FACTORY SUCCESS......");
    }
    } catch (Exception e) {
    logger.error("STOP ACTIVEMQ CONNECTION FACTORY ERROR!!!", e);
    }
    }

    /**
    * 从连接池获取链接
    * @author 程松
    * @date 2013-12-7 下午01:46:02
    * @return
    * @see [类、类#方法、类#成员]
    */
    public static Connection getConnection(){
    try {
    //从连接池工厂中获取一个连接
    Connection connection = connectionFactory.createConnection();
    connection.start();
    return connection;
    } catch (JMSException e) {
    e.printStackTrace();
    }
    return null;
    }

    /**
    * 链接打开session回话
    * @author 程松
    * @date 2013-12-7 下午01:46:19
    * @param conn
    * @return
    * @see [类、类#方法、类#成员]
    */
    public static Session getSession(Connection conn){
    Session session = null;
    try {
    //false 参数表示 为非事务型消息,后面的参数表示消息的确认类型
    session = conn.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);
    } catch (JMSException e) {
    e.printStackTrace();
    }
    return session;
    }

    /**
    * 创建一个生产者
    * @author 程松
    * @date 2013-12-7 下午01:48:14
    * @param session
    * @return
    * @see [类、类#方法、类#成员]
    */
    public static MessageProducer getProducer(Session session, String queue_name){
    try {
    Destination destination = session.createQueue(queue_name);
    MessageProducer producer = session.createProducer(destination);
    producer.setDeliveryMode(DeliveryMode.PERSISTENT);
    return producer;
    } catch (Exception e) {
    e.printStackTrace();
    }
    return null;
    }

    /**
    * 创建一个消费者
    * @author 程松
    * @date 2013-12-7 下午01:53:28
    * @param session
    * @param queue_name
    * @return
    * @see [类、类#方法、类#成员]
    */
    public static MessageConsumer getConsumer(Session session, String queue_name){
    try {
    Destination destination = session.createQueue(queue_name);
    MessageConsumer consumer = session.createConsumer(destination);
    return consumer;
    } catch (Exception e) {
    e.printStackTrace();
    }
    return null;
    }

    /**
    * 创建文本消息
    * @author 程松
    * @date 2013-12-7 下午02:05:05
    * @param session
    * @param msg
    * @return
    * @throws JMSException
    * @see [类、类#方法、类#成员]
    */
    public static TextMessage getMessage(Session session, String msg) throws JMSException {
    TextMessage message = session.createTextMessage(msg);
    return message;
    }

    /**
    * 创建对象消息
    * @author 程松
    * @date 2013-12-7 下午02:06:51
    * @param session
    * @param obj
    * @return
    * @throws JMSException
    * @see [类、类#方法、类#成员]
    */
    public static ObjectMessage getMessage(Session session, Serializable obj) throws JMSException {
    ObjectMessage message = session.createObjectMessage(obj);
    return message;
    }

    /**
    * 创建map消息
    * @author 程松
    * @date 2013-12-7 下午02:17:58
    * @param session
    * @param map
    * @return
    * @throws JMSException
    * @see [类、类#方法、类#成员]
    */
    public static MapMessage getMessage(Session session, Map map) throws JMSException {
    MapMessage message = session.createMapMessage();
    Set<String> set = map.keySet();
    Iterator<String> it = set.iterator();
    while(it.hasNext()){
    String key = it.next();
    String value = String.valueOf(map.get(key));
    message.setString(key, value);
    }
    return message;
    }

    /**
    * 发送文本消息
    * @author 程松
    * @date 2013-12-7 下午02:30:46
    * @param model
    * @see [类、类#方法、类#成员]
    */
    public static void send(final String msg, final String queue_name) {
    //直接使用线程池来执行具体的调用
    threadPool.execute(new Runnable(){
    public void run() {
    try {
    sendMsg(msg, queue_name);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    });
    }

    /**
    * 真正的执行消息发送
    * @author 程松
    * @date 2013-12-7 下午02:33:02
    * @param msg
    * @param queue_name
    * @throws Exception
    * @see [类、类#方法、类#成员]
    */
    private static void sendMsg(String msg, String queue_name) throws Exception {

    Connection connection = null;
    Session session = null;
    try {
    //从连接池工厂中获取一个连接
    connection = ActivemqConnectionFactory.getConnection();
    session = ActivemqConnectionFactory.getSession(connection);
    MessageProducer producer = ActivemqConnectionFactory.getProducer(session, queue_name);
    Message message = ActivemqConnectionFactory.getMessage(session, msg);
    producer.send(message);
    } finally {
    ActivemqConnectionFactory.closeSession(session);
    ActivemqConnectionFactory.closeConnection(connection);
    }
    }

    /**
    * 发送对象消息
    * @author 程松
    * @date 2013-12-7 下午02:33:40
    * @param model
    * @param queue_name
    * @see [类、类#方法、类#成员]
    */
    public static void send(final QueueModel model, final String queue_name) {
    //直接使用线程池来执行具体的调用
    threadPool.execute(new Runnable(){
    public void run() {
    try {
    sendMsg(model, queue_name);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    });
    }

    /**
    * 真正的执行消息发送
    * @author 程松
    * @date 2013-12-7 下午02:36:44
    * @param model
    * @param queue_name
    * @throws Exception
    * @see [类、类#方法、类#成员]
    */
    private static void sendMsg(QueueModel model, String queue_name) throws Exception {

    Connection connection = null;
    Session session = null;
    try {
    //从连接池工厂中获取一个连接
    connection = ActivemqConnectionFactory.getConnection();
    session = ActivemqConnectionFactory.getSession(connection);
    MessageProducer producer = ActivemqConnectionFactory.getProducer(session, queue_name);
    Message message = ActivemqConnectionFactory.getMessage(session, model);
    producer.send(message);
    } finally {
    ActivemqConnectionFactory.closeSession(session);
    ActivemqConnectionFactory.closeConnection(connection);
    }
    }

    /**
    * 发送MAP消息
    * @author 程松
    * @date 2013-12-7 下午02:34:29
    * @param model
    * @see [类、类#方法、类#成员]
    */
    public static void send(final Map map, final String queue_name) {
    //直接使用线程池来执行具体的调用
    threadPool.execute(new Runnable(){
    public void run() {
    try {
    sendMsg(map, queue_name);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    });
    }

    /**
    * 真正的执行消息发送
    * @author 程松
    * @date 2013-12-7 下午02:36:14
    * @param map
    * @param queue_name
    * @throws Exception
    * @see [类、类#方法、类#成员]
    */
    private static void sendMsg(Map map, String queue_name) throws Exception {

    Connection connection = null;
    Session session = null;
    try {
    //从连接池工厂中获取一个连接
    connection = ActivemqConnectionFactory.getConnection();
    session = ActivemqConnectionFactory.getSession(connection);
    MessageProducer producer = ActivemqConnectionFactory.getProducer(session, queue_name);
    Message message = ActivemqConnectionFactory.getMessage(session, map);
    producer.send(message);
    } finally {
    ActivemqConnectionFactory.closeSession(session);
    ActivemqConnectionFactory.closeConnection(connection);
    }
    }



    /**
    * 获取文本消息
    * @author 程松
    * @date 2013-12-7 下午02:18:13
    * @param message
    * @return
    * @see [类、类#方法、类#成员]
    */
    public static String getText(Message message){
    if (message instanceof TextMessage) {
    //强制转换一下
    TextMessage txtMsg = (TextMessage) message;
    //输出接收到的消息
    String model = null;
    try {
    model = txtMsg.getText();
    return model;
    } catch (JMSException e) {
    e.printStackTrace();
    }
    }
    return null;
    }

    /**
    * 获取对象消息
    * @author 程松
    * @date 2013-12-7 下午02:18:41
    * @param message
    * @return
    * @see [类、类#方法、类#成员]
    */
    public static Object getObject(Message message){
    if (message instanceof ObjectMessage) {
    //强制转换一下
    ObjectMessage txtMsg = (ObjectMessage) message;
    //输出接收到的消息
    Object model = null;
    try {
    model = txtMsg.getObject();
    return model;
    } catch (JMSException e) {
    e.printStackTrace();
    }
    }
    return null;
    }

    /**
    * 获取map消息
    * @author 程松
    * @date 2013-12-7 下午02:19:00
    * @param message
    * @return
    * @see [类、类#方法、类#成员]
    */
    public static Map getMap(Message message){
    if (message instanceof MapMessage) {
    //强制转换一下
    MapMessage txtMsg = (MapMessage) message;
    //输出接收到的消息
    Map map = new HashMap();
    try {
    Enumeration<String> e = txtMsg.getMapNames();
    while(e.hasMoreElements()){
    String key = e.nextElement();
    map.put(key, txtMsg.getString(key));
    }
    return map;
    } catch (JMSException e) {
    e.printStackTrace();
    }
    }
    return null;
    }

    /**
    * 关闭回话
    * @author 程松
    * @date 2013-12-7 下午01:46:55
    * @param session
    * @see [类、类#方法、类#成员]
    */
    public static void closeSession(Session session) {
    try {
    if (session != null) {
    session.close();
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    /**
    * 关闭链接
    * @author 程松
    * @date 2013-12-7 下午01:47:06
    * @param connection
    * @see [类、类#方法、类#成员]
    */
    public static void closeConnection(Connection connection) {
    try {
    if (connection != null) {
    connection.close();
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    }

    spring.xml中配置:

    <!-- actviemq factory -->
    <bean class="com.lejob.lejobsearch.index.queue.ActivemqConnectionFactory" init-method="init" destroy-method="destroy">
    </bean>

    ,项目启动时加载初始化ActivemqConnectionFactory。

    项目中ActivemqConnectionFactory.send(param,queue_name);发送消息。

    下面是消息消费者:IMessageHandler.java(消息处理任务接口)、RecrQueueMessageHandler.java(消息处理类)、RecrQueueConsumer.java(JMS消息消费者)、MultiThreadMessageListener.java(消息消费者中使用的多线程消息监听服务)、FixedAndBlockedThreadPoolExecutor.java(支持阻塞的固定大小的线程池)

    IMessageHandler.java

    package com.lejob.lejobsearch.index.queue;

    import javax.jms.Message;

    /**
    * 提供消息操作的回调接口
    *
    * @author 程松
    * @date 2013-12-7上午10:49:49
    * @company 乐享网络(5lejob)北京研发中心
    * @version [Copyright (c) 2013 V001]
    * @see [相关类/方法]
    * @since [产品/模块版本]
    */
    public interface IMessageHandler {

    /**
    * 消息回调提供的调用方法
    * @author 程松
    * @date 2013-12-7 上午10:28:58
    * @param message
    * @see [类、类#方法、类#成员]
    */
    public void handle(Message message);

    }

    RecrQueueConsumer.java

    package com.lejob.lejobsearch.index.queue.recruitment;

    import javax.jms.Connection;
    import javax.jms.ExceptionListener;
    import javax.jms.JMSException;
    import javax.jms.MessageConsumer;
    import javax.jms.Session;

    import org.apache.log4j.Logger;

    import com.lejob.lejobsearch.index.queue.ActiveMQConstant;
    import com.lejob.lejobsearch.index.queue.ActivemqConnectionFactory;
    import com.lejob.lejobsearch.index.queue.IMessageHandler;
    import com.lejob.lejobsearch.index.queue.MultiThreadMessageListener;


    /**
    * JMS消息消费者
    *
    * @author 程松
    * @date 2013-11-30下午04:00:09
    * @company 乐享网络(5lejob)北京研发中心
    * @version [Copyright (c) 2013 V001]
    * @see [相关类/方法]
    * @since [产品/模块版本]
    */
    public class RecrQueueConsumer implements ExceptionListener {

    private Logger logger = Logger.getLogger(RecrQueueConsumer.class);

    /**
    * 监听消息线程最大值
    */
    private static final int THREAD_COUNT = 1;
    /**
    * 消息处理类
    */
    private IMessageHandler messageHandler;

    private Connection connection;
    private Session session;

    public void init(){
    try {
    connection = ActivemqConnectionFactory.getConnection();
    //会话采用非事务级别,消息到达机制使用自动通知机制
    session = ActivemqConnectionFactory.getSession(connection);
    MessageConsumer consumer = ActivemqConnectionFactory.getConsumer(session, ActiveMQConstant.RECR_QUEUE);
    consumer.setMessageListener(new MultiThreadMessageListener(THREAD_COUNT, messageHandler));
    logger.info(ActiveMQConstant.RECR_QUEUE + " LISTENER INIT SUCCESS......");
    } catch (Exception e) {
    logger.error("处理队列" + ActiveMQConstant.RECR_QUEUE + "信息 异常", e);
    }
    }
    public void destroy(){
    try {
    ActivemqConnectionFactory.closeSession(session);
    ActivemqConnectionFactory.closeConnection(connection);
    logger.info(ActiveMQConstant.RECR_QUEUE + " LISTENER DESTROY SUCCESS......");
    } catch (Exception e) {
    logger.error("关闭监听队列" + ActiveMQConstant.RECR_QUEUE + " 异常", e);
    }
    }

    public void onException(JMSException e) {
    e.printStackTrace();
    }

    public void setMessageHandler(IMessageHandler messageHandler) {
    this.messageHandler = messageHandler;
    }



    }

    MultiThreadMessageListener.java

    package com.lejob.lejobsearch.index.queue.recruitment;

    import java.util.concurrent.ExecutorService;

    import javax.jms.JMSException;
    import javax.jms.Message;
    import javax.jms.MessageListener;
    import javax.jms.ObjectMessage;

    import com.lejob.lejobsearch.index.queue.MessageHandler;
    import com.lejob.rpc.search.model.QueueModel;

    /**
    * 消息消费者中使用的多线程消息监听服务
    *
    */
    public class MultiThreadMessageListener implements MessageListener {

    //默认线程池数量
    public final static int DEFAULT_HANDLE_THREAD_POOL=10;
    //最大的处理线程数.
    private int maxHandleThreads;
    //提供消息回调调用接口
    private MessageHandler messageHandler;

    private ExecutorService handleThreadPool;


    public MultiThreadMessageListener(MessageHandler messageHandler){
    this(DEFAULT_HANDLE_THREAD_POOL, messageHandler);
    }

    public MultiThreadMessageListener(int maxHandleThreads,MessageHandler messageHandler){
    this.maxHandleThreads=maxHandleThreads;
    this.messageHandler=messageHandler;
    //支持阻塞的固定大小的线程池(自行手动创建的)
    this.handleThreadPool = new FixedAndBlockedThreadPoolExecutor(this.maxHandleThreads);
    }


    /**
    * 监听程序中自动调用的方法
    */
    public void onMessage(final Message message) {
    //使用支持阻塞的固定大小的线程池来执行操作
    this.handleThreadPool.execute(new Runnable() {
    public void run() {
    try {
    if (message instanceof ObjectMessage) {
    //强制转换一下
    ObjectMessage txtMsg = (ObjectMessage) message;
    //输出接收到的消息
    QueueModel model = null;
    try {
    model = (QueueModel) txtMsg.getObject();
    MultiThreadMessageListener.this.messageHandler.handle(model);
    } catch (JMSException e) {
    e.printStackTrace();
    }
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    });
    }

    }

    FixedAndBlockedThreadPoolExecutor.java

    package com.lejob.lejobsearch.index.queue.recruitment;

    import java.util.concurrent.LinkedBlockingQueue;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.ReentrantLock;

    /**
    * 支持阻塞的固定大小的线程池
    *
    */
    public class FixedAndBlockedThreadPoolExecutor extends ThreadPoolExecutor {


    //一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
    //使用 lock 块来调用 try,在之前/之后的构造中
    private ReentrantLock lock = new ReentrantLock();

    private Condition condition = this.lock.newCondition();

    public FixedAndBlockedThreadPoolExecutor(int size) {
    super(size, size, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
    }


    /**
    * 当线程池中没有空闲线程时,会挂起此方法的调用线程.直到线程池中有线程有空闲线程.
    */
    @Override
    public void execute(Runnable command) {
    //进行同步锁定
    this.lock.lock();
    super.execute(command);
    try {
    //如果线程池的数量已经达到最大线程池的数量,则进行挂起操作
    if (getPoolSize() == getMaximumPoolSize()) {
    this.condition.await();
    }
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    this.lock.unlock();
    }
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
    super.afterExecute(r, t);
    try {
    this.lock.lock();
    this.condition.signal();
    } finally {
    this.lock.unlock();
    }
    }


    }

    项目spring.xml文件中配置

    <!-- 任务处理类 -->

    <bean id="recrQueueMessageHandler" class="com.lejob.lejobsearch.index.queue.recruitment.RecrQueueMessageHandler">
    </bean>


    <!-- JMS消息消费者 -->

    <bean class="com.lejob.lejobsearch.index.queue.recruitment.RecrQueueConsumer" init-method="init" destroy-method="destroy">
    <property name="messageHandler" ref="recrQueueMessageHandler"></property>
    </bean>

    本文参考:http://blog.csdn.net/linwei_1029/article/details/16964943

  • 相关阅读:
    数组去重
    css盒模型
    px、em、rem的区别
    Html5新标签
    弹性布局
    相对定位与绝对定位
    Hadoop综合大作业
    分布式文件系统HDFS 练习
    安装Hadoop
    爬虫综合大作业
  • 原文地址:https://www.cnblogs.com/dead-trap-ramble/p/3450745.html
Copyright © 2011-2022 走看看