码迷,mamicode.com
首页 > 其他好文 > 详细

Servlet

时间:2019-08-22 23:47:10      阅读:206      评论:0      收藏:0      [点我收藏+]

标签:邮件服务   地址   throwable   fresh   刷新   simple   serial   rom   field   

TestServlet.java

// 文件路径 D:\ApacheServer\web_java\HelloWorld\src\com\test\TestServlet.java
package com.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.SQLException;
import java.sql.PreparedStatement;
import java.text.SimpleDateFormat;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpSession;



import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.fileupload.FileUploadException;

import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;


// 由于 web.xml 里配置的 url-pattern = /TomcatTest/TestServlet ,所以该 Servlet 浏览地址可以是 http://localhost:8080/TomcatTest/TestServlet
// 下面的注解 @WebServlet 功能和 web.xml 配置 url-pattern 类似,所以该 Servlet 浏览地址也可以是 http://localhost:8080/TestServlet
// 注解浏览地址和 web.xml 里配置的 url-pattern 地址不能一样,要么只配置其中一项(删除注解,或者删除web.xml中对应Servlet的<servlet-mapping>...</servlet-mapping>项),要么两者配置地址不能相同
@WebServlet("/TestServlet")
public class TestServlet extends HttpServlet {
    // 实际作用不明,暂时注释,无影响
    //private static final long serialVersionUID = 1L;
    
    public TestServlet() {
        super();
    }
    
    public void init() throws ServletException {
        // 创建 Servlet 时只执行一次的 init
    }
    
    public void destroy() {
        // 销毁 Servlet 时只执行一次的 destroy
        // destroy 方法被调用后,servlet 被销毁,但是并没有立即被回收,再次请求时,并没有重新初始化。
    }

    
    // post 请求会被 doPost 处理
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 转移到 doGet 函数处理信息
        doGet(request, response);
    }
    
    // get 请求会被 doGet 处理
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //response.getWriter().append("Served at1: ").append(request.getContextPath());
        
        /*
        // 配合测试异常处理 Servlet (这里是 TestErrorServlet )捕获处理 web 容器抛出的异常
        if(true) {
            response.setContentType("text/html;charset=UTF-8");
            throw new ServletException("这是测试异常信息");
        }
        */
        
        // ============================= response 响应 ============================================
        // 设置返回响应内容类型及编码,否则返回输出中文会乱码
        response.setContentType("text/html;charset=UTF-8");
        // 添加一个自定义名称和值的响应报头。
        response.setHeader("test_foo", "test_val");
        // 响应返回任意错误响应状态码及信息(通常为404 或407等状态码),该设置会使页面直接显示错误页面
        //response.sendError(404,"test response status");
        // 返回任意响应状态码
        //response.setStatus(200);
        // 生成一个 302 响应,暂时性重定向到后面的网址
        //response.sendRedirect("http://www.baidu.com");
        
        // ============================= 请求的 HTTP 头信息 ============================================
        // 获取打印客户端请求的 HTTP 头信息
        Enumeration headerNames = request.getHeaderNames();
        while(headerNames.hasMoreElements()) {
            String paramName = (String)headerNames.nextElement();
            String paramValue = request.getHeader(paramName);
            response.getWriter().append("<br/>客户端请求的 HTTP 头信息 : " + paramName + " 值为 : " + paramValue);
        }
        
        // ============================= Servlet Cookie ============================================
        // Servlet Cookie 处理需要对中文进行编码与解码,方法如下
        String enStr = java.net.URLEncoder.encode("测试中文", "UTF-8"); // 编码
        String deStr = java.net.URLDecoder.decode(enStr, "UTF-8"); // 解码
        
        // 创建新 cookie 对象 并赋键值对,键值即该 cookie 名称,这里值赋中文值,需要先编码
        Cookie cookie = new Cookie("testKey",enStr);
        // 设置 cookie 适用的域名,例如 runoob.com
        cookie.setDomain("runoob.com");
        // 获取 cookie 适用的域,例如 runoob.com
        cookie.getDomain();
        // 设置 cookie 过期的时间(以传给客户端为起始,以秒为单位)。如果不设置,cookie 只会在当前 session 会话中持续有效。或者cookie.setMaxAge(0) 即表示删除这个cookie
        cookie.setMaxAge(3600 * 24);
        // 返回 cookie 的最大生存周期(以秒为单位),默认情况下,-1 表示 cookie 将持续下去,直到浏览器关闭。
        cookie.getMaxAge();
        // 返回 cookie 的名称。名称在创建后不能改变。
        cookie.getName();
        // 设置 cookie 的值,除了创建 cookie 对象时赋值,setValue() 方法也可赋值。赋的值非中文就不用转码了
        cookie.setValue("newTestVal");
        // 获取 cookie 的值
        cookie.getValue();
        // 设置 cookie 适用的路径uri。浏览器在发该 cookie 消息给服务器之前,请求的 url 中必须存在一个指定 uri 路径。这个比较是通过将 path 属性值与请求的 url 从头开始逐字符串比较完成的。如果字符匹配,则发送该 cookie 消息。如果不指定路径,与当前页面相同目录(uri)下的(包括子目录下的)所有 url 浏览器都会返回 cookie。
        cookie.setPath("/testuri");
        // 获取 cookie 适用的路径uri。
        cookie.getPath();
        // 设置布尔值,表示 cookie 是否应该只在加密的(即 SSL)连接上发送。
        cookie.setSecure(false);
        // 设置该 cookie 在浏览器中不能通过 Javascript 的 document.cookie 属性访问
        cookie.setHttpOnly(true);
        // 设置cookie的注释。该注释在浏览器向用户呈现 cookie 时非常有用。
        cookie.setComment("testNotes");
        // 获取 cookie 的注释,如果 cookie 没有注释则返回 null。
        cookie.getComment();
        
        // 输出 cookie值,中文的话需要解码
        response.getWriter().append("<br/>cookie testKey 值为 : " + java.net.URLDecoder.decode(cookie.getValue(), "UTF-8"));
        
        // 发送 Cookie 到 HTTP 响应头,即将 cookie 传到浏览器
        response.addCookie(cookie);
        
        
        // 获取览器发送的与当前页面 url 相关的 Cookie 的数组
        Cookie request_cookie[] = request.getCookies();
        if( request_cookie != null ){
            // 遍历获取每一个浏览器发送的 cookie
            for (int i = 0; i < request_cookie.length; i++){
                cookie = request_cookie[i];
                // 对比找到指定名称的 cookie
                if((cookie.getName( )).compareTo("delCookieName") == 0 ){
                    // 然后删除
                    cookie.setMaxAge(0);
                    response.addCookie(cookie);
                }else {
                    response.getWriter().append("<br/>浏览器上传 cookie 名称 : " + cookie.getName() + " 值为 : " + cookie.getValue());
                }
            }
        }else{
            response.getWriter().append("<br/>No Cookie founds");
        }
        
        
        
        
        // ============================= Servlet Session ============================================
        // 获取当前请求的 session 会话,需要在向客户端发送任何文档内容之前调用 request.getSession()。
        // getSession() 等同于 getSession(true) 若存在会话,则返回该会话,否则新建一个会话。getSession(false) 则是如存在会话,则返回该会话,否则返回NULL
        HttpSession session = request.getSession();
        
        //下面总结了 HttpSession 对象中可用的几个重要的方法:
        // 指定键名绑定一个对象到该 session 会话,绑定的对象可以是任意类型的对象包括字符串
        Object anyObject = new Object();
        session.setAttribute("testKey", anyObject);
        // 获取 session 中指定键名的数据值,如果没有指定该键名对则返回 null,之前赋的键值无论何类型,getAttribute 方法返回的都是 Object 类型,使用时需要强制类型
        Object val = session.getAttribute("testKey");
        // 从该 session 会话移除指键名称及对应对象。
        session.removeAttribute("testKey");
        // 以枚举方式获取 session 中所有设置绑定的键名
        Enumeration sessionEnumeration = session.getAttributeNames();
        
        // 获取 session id
        String sessionId = session.getId();
        // 返回该 session 创建的时间戳,单位毫秒,创建完不一定已经回传给客户端。
        long sessionBegin = session.getCreationTime();
        // 判断该对话是否是第一次建立,即客户端还未保存 session id,或者客户选择不参入该 session 会话,则该方法返回 true
        boolean isNew = session.isNew();
        // 返回该 session 客户端最后一次请求到服务器的时间戳,单位毫秒
        long sessionEnd = session.getLastAccessedTime();
        // 设定 session 在用户请求间隔多少秒内不失效
        session.setMaxInactiveInterval(360);
        // 返回 Servlet 容器 session 在用户请求间隔多少秒内不失效
        int interval = session.getMaxInactiveInterval();
        // 使该 session 会话无效,并解除绑定到它上面的任何对象。
        //session.invalidate();
        
        // 输出相关信息
        // session 创建时间
        Date createTime = new Date(sessionBegin);
        // 该网页的最后一次访问时间
        Date lastAccessTime = new Date(sessionEnd);
        //设置日期输出的格式
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        // 设置键值
        String testStr = "";
        int testInt = 0;
        if(session.getAttribute("key_str") == null) {
            testStr = "this is test str";
            session.setAttribute("key_str", testStr);
        }else {
            testStr = (String)session.getAttribute("key_str");
        }
        if(session.getAttribute("key_int") == null) {
            testInt = 0;
            session.setAttribute("key_int", testInt);
        }else {
            testInt = (int)session.getAttribute("key_int") + 1;
            session.setAttribute("key_int", testInt);
        }
        response.getWriter().append("<br/>用户最后一次请求时间为 : " + dateFormat.format(lastAccessTime));
        response.getWriter().append("<br/>session key_str 键对应值为 : " + testStr);
        response.getWriter().append("<br/>session testInt 键对应值为 : " + testInt);
        
        
        /*
        // ============================= 下载文件 ============================================
        // 最好放在 response 所有输出之前调用
        this.download(response);
        // 为禁止再向浏览器发送其他输出流
        if(true) return;
        */
        
        
        // ============================= 连接 MySql ============================================
        this.conn_mysql(response);
        
        
        // ============================= 日期处理 ============================================
        doDate(response);
        
        // ============================= 网页重定向 ============================================
        // 浏览器重新请求的新地址,URL会变
        String location = "https://www.baidu.com" ;
        // 方法一
        //response.sendRedirect(location);
        // 方法二
        //response.setStatus(response.SC_MOVED_TEMPORARILY);
        //response.setHeader("Location", location);
        
        // ============================= 自动刷新页面 ============================================
        // 设置自动刷新间隔为 5 秒
        //response.setIntHeader("Refresh", 5);
        
        // ============================= 发送邮件 ============================================
        sendMail(request, response);
        
        
        // 获取 form-data 类型 post 数据依赖于 FileUpload,下载地址 http://commons.apache.org/proper/commons-fileupload/ 这里用到的是 FileUpload 1.4 选择 Binaries->commons-fileupload-1.4-bin.zip
        // FileUpload 依赖于 Commons IO,下载地址 http://commons.apache.org/proper/commons-io/ 这里用到的是 Commons IO 2.6 选择 Binaries->commons-io-2.6-bin.zip
        // 将下载的压缩包内的 commons-io-2.6.jar 和 commons-fileupload-1.4.jar 解压缩到 D:\ApacheServer\web_java\HelloWorld\WebContent\WEB-INF\lib 中。环境变量 CLASSPATH 补充 ";D:\ApacheServer\web_java\HelloWorld\WebContent\WEB-INF\lib\commons-io-2.6.jar;D:\ApacheServer\web_java\HelloWorld\WebContent\WEB-INF\lib\commons-fileupload-1.4.jar;" 。eclipse->Java Build Path 中分别引入 commons-io-2.6.jar 与 commons-fileupload-1.4.jar
        
        // 检测是 GET 和  x-www-form-urlencode POST 还是 multipart/form-data POST 方式
        if (!ServletFileUpload.isMultipartContent(request)) {
            // 提交的表单类型为GET 或者 x-www-form-urlencoded 方式的 POST 
            getOrFormUrlencoded(request, response);
        }else {
            // 提交的表单类型为 multipart/form-data 的 POST
            formData(request, response);
        }
        
    }

    
    // 自建方法,文件下载
    public void download(HttpServletResponse response){
        try{
            // 要下载的文件名,必须是服务器上存在的文件,eclipse 实际运行项目路径需要找找
            // 这里实际目录为 D:/ApacheServer/web_java/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/HelloWorld/upload/00125943U-0.jpg
            String fileName = "00125943U-0.jpg";
            // 文件下载到客户端时文件名
            String downName = "download.jpg";
            // 设置文件 MIME 类型,getServletContext()为父类方法
            response.setContentType(this.getServletContext().getMimeType(fileName));
            // 设置 Content-Disposition 响应报头,命名下载文件名称
            response.setHeader("Content-Disposition"," attachment;filename=" + downName);
            
            ServletContext context = this.getServletContext();
            // 根据文件在项目中的路径获取文件的完整绝对路径,这里 upload 文件夹为项目根目录下,与 WEB-INF 文件夹同级父目录下
            String fullFileName = context.getRealPath("/upload/" + fileName);
            
            // 把文件读入到内存输入流中
            InputStream inputStream = new FileInputStream(fullFileName);
            // 创建输出流对象
            ServletOutputStream outputStream = response.getOutputStream();
            
            // 每次从输入流实际读取到的字节数
            int len = 0;
            // 定义一个字节数组,相当于缓存,数组长度为1024,即缓存大小为1024个字节
            byte[] cache = new byte[1024];
            // inputStream.read(cache)) 方法,从输入流中读取最多 cache 数组大小的字节,并将其存储在 cache 中。以整数形式返回实际读取的字节数,当文件读完时返回-1
            while((len = inputStream.read(cache)) != -1){
                // 每次把数组 cache 从 0 到 len 长度的内容输出到浏览器
                outputStream.write(cache, 0, len);
            }
            // 关闭流
            inputStream.close();
            outputStream.close();
        }catch(Exception exception) {
            // 处理 Class.forName 错误
            exception.printStackTrace();
        }
        
    }
    // 自建方法,连接 MySQL 数据库
    public void conn_mysql(HttpServletResponse response) {
        // 需要下载 MySQL 驱动 jar 包,下载地址 https://mvnrepository.com/artifact/mysql/mysql-connector-java,这里用到的是 mysql-connector-java-8.0.17.jar,下载的 jar 包 要放在 Tomcat 安装目录下的 lib 文件夹内,这里路径是 D:\ApacheServer\apache-tomcat\lib\mysql-connector-java-8.0.17.jar
        // 连接 MySQL 需要 MySQL 的时区和 java 一致(MySQL 默认 UTC 时区),这里本地是北京时区。修改  MySQL 配置文件 my.ini 在 [mysqld] 项下添加一行 default-time-zone = ‘+8:00‘ 重启 MySQL 即可。如果不能修改MySQL配置文件,则将如下 jdbc:mysql://localhost:3306/testdb 改为 jdbc:mysql://localhost:3306/testdb?serverTimezone=UTC 亦可
        try{
            // 注册 JDBC 驱动器
            // 如果下载的 MySQL jar 包是 5 系列版本,则需要用 com.mysql.jdbc.Driver 注册驱动,而且必须写
            //Class.forName("com.mysql.jdbc.Driver");
            // MySQL jar包 6 以上版本用 com.mysql.cj.jdbc.Driver 注册,或者不写也可,这里没有写,已注释掉
            //Class.forName("com.mysql.cj.jdbc.Driver");
            // 打开一个连接
            Connection conn = null;
            // localhost 为数据库连接地址,3306 端口号,testDB 为数据库名称
            String db_host  = "jdbc:mysql://localhost:3306/testdb";
            // 用户名
            String user     = "root";
            // 密码
            String password = "123456";
            conn = DriverManager.getConnection(db_host,user,password);
            
            // ==================================================== Statement 类处理
            // Statement 对象执行查询 SQL 语句
            Statement statement = conn.createStatement();
            String sql_sel      = "SELECT id, name, age FROM test_table";
            ResultSet result    = statement.executeQuery(sql_sel);
            //遍历结果集
            int i = 1;
            while(result.next()){
                int id      = result.getInt("id");
                String name = result.getString("name");
                int age     = result.getInt("age");
                response.getWriter().append("<br/>第 " + i + "行 id : " + id + " name : " + name + " age : " + age);
                i++;
            }
            // ==================================================== PreparedStatement 类处理
            // PreparedStatement 执行插入语句效果更好,原因:1可以写动态参数化的查询,2PreparedStatement比 Statement 更快,3PreparedStatement可以防止SQL注入式攻击
            // preparedStatement对象的 executeUpdate() 方法可执行 insert,update,delete 语句以及 SQL DDL(如建表,改表,删除表等), executeQuery() 方法执行select语句,execute()执行所有语句,效率也最慢
            // ============================ executeUpdate 方法
            //编写预处理 SQL 语句
            String sql_ins= "INSERT INTO `test_table`(id, name, age) VALUES(?, ?, ?)";
            PreparedStatement preparedStatement = conn.prepareStatement(sql_ins);
            preparedStatement.setInt(1, 3); //插入表数据的 id
            preparedStatement.setString(2, "testName"); // 名称
            preparedStatement.setInt(3, 40); // 年龄
            // 返回值是一个整数,指示受影响的行数(即更新计数)建表,删表等不操作行的语句总返回 0。
            int updateCount = preparedStatement.executeUpdate();
            response.getWriter().append("<br/> 插入结果 " + updateCount);
            // ============================ executeQuery 方法
            // 执行 select 语句
            preparedStatement = conn.prepareStatement(sql_sel);
            // 获取查询结果,result 遍历查询结果与之前 Statement 类方式一致
            result = preparedStatement.executeQuery();
            // ============================ execute 方法
            preparedStatement = conn.prepareStatement(sql_sel);
            // execute 可用于执行任何SQL语,返回一个 boolean 值,如果执行后第一个结果(有可能返回的是多个结果集合)是 ResultSet(即查询语句结果),则返回 true,否则返回 false。
            boolean isResult = preparedStatement.execute();
            // 如果 execute() 执行的 sql 语句能返回多个结果集合时,PreparedStatement 对象获取下个 getResultSet() 或者 getUpdateCount() 前要先执行 getMoreResults() 使指针下移,然后再执行 getResultSet() 或 getUpdateCount() 获取当前指针指向结果
            //preparedStatement.getMoreResults(); // 一至多个结果集时,获取第一个结果时不需要执行此语句,获取下一个结果时才执行,如在 while 循环语句里用
            if(isResult) {
                // 如果结果集当前指针指向的返回结果是个 select 查询结果,用 getResultSet 获取,如果执行的是更新语句,则返回的是更新计数,这时就要用 getUpdateCount来获取
                result = preparedStatement.getResultSet();
                // ...
            }else {
                //当某个过程返回两个更新计数,则首先调用方法getUpdateCount()
                updateCount = preparedStatement.getUpdateCount();
                response.getWriter().append("<br/> 插入结果 " + updateCount);
            }
            // 完成后关闭
            preparedStatement.close();
            result.close();
            statement.close();
            conn.close();
        }catch(SQLException sqlException) {
            // 处理 JDBC 错误
            sqlException.printStackTrace();
        }catch(Exception exception) {
            // 处理 Class.forName 错误
            exception.printStackTrace();
        }
    }
    
    // 自建方法,处理日期
    public void doDate(HttpServletResponse response) throws ServletException, IOException {
        Date date = null;
        // 构造函数初始化当前日期和时间的对象
        date = new Date();
        // 或者构造指定日期时间的对象。参数为毫秒时间戳,示例时间为 2019/8/18 16:12:56
        date = new Date(1566115976000L);
        // date 对应的时间戳大于 after() 的参数所对应时间戳则返回 true,否则返回 false。这里返回 false
        if(date.after(new Date())) {
            response.getWriter().append("<br/> date对应时间大于 after 参数对应时间");
        }else {
            response.getWriter().append("<br/> date对应时间小于 after 参数对应时间");
        }
        // 判断方式与 date.after() 相反
        boolean isBefore = date.before(new Date());
        // 克隆一个时间对象,但返回值类型为 Object
        Object objDate = date.clone();
        // 如果两个 date 对象时间戳相等,返回0,如果 date 大于 compareTo() 里参数的时间,则返回正数,否则返回负数,这里结果为 -1
        int compare = date.compareTo(new Date());
        response.getWriter().append("<br/> 对比时间结果为 : " + compare);
        // 两日期对象对应时间戳相等返回 true,否则返回 false,这里返回 true
        boolean equals = date.equals(new Date(1566115976000L));
        // 获取当前 date 对应时间戳
        long stampLong = date.getTime();
        response.getWriter().append("<br/> 当前 date 时间戳为 : " + stampLong);
        // 返回该日期对象的哈希码值
        int hashCode = date.hashCode();
        // 设置 date 对象对应的时间戳
        date.setTime(1566119343000L);
        // 将 date 对象转换为字符串,这里返回为 Sun Aug 18 17:09:03 CST 2019
        String strDate = date.toString();
        response.getWriter().append("<br/> 当前 date 字符串为 : " + strDate);
        // 创建日期格式化对象,y 年,M 月,d 月中第几日,H 小时(24制),h 小时(12制), m 分,s 秒,S 毫秒,E 星期几,D 年中第几日,F 月中第几周,w 年中第几周,W 月中的第几周,a 上午下午,k 天中第几小时,K 带有 A.M./P.M. 的小时(0~11),z 时区
        SimpleDateFormat dateFormat = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss");
        String strFormat = dateFormat.format(date);
        // 这里输出日期为 2019-08-18 17:09:03
        response.getWriter().append("<br/> 格式化后时间为 : " + strFormat);
    }
    
    // 自建方法,发送邮件
    public void sendMail(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 需要先下载 javax.mail.jar 和 activation.jar。只发文字邮件 javax.mail.jar 就够了,发附件则还需要 activation.jar
        // mail.jar 下载地址 https://www.oracle.com/technetwork/java/javamail/index.html 点击 Downloads 下载 JavaMail API 1.4.7,或者 https://javaee.github.io/javamail/ 下载 JavaMail 1.6.2 这里版本是 JavaMail 1.6.2 
        // jaf 下载页面 https://www.oracle.com/technetwork/java/javasebusiness/downloads/java-archive-downloads-java-plat-419418.html 这里用到版本 JavaBeans Activation Framework 1.1.1
        // 提取下载的压缩包里的 javax.mail.jar 和 activation.jar 放到项目中
        
        
        // 收件人的电子邮件 ID
        String to = "32705317@qq.com";
        // 发件人的电子邮件 ID
        String from = "zdy_521@126.com";
        // SMTP服务器地址
        String host = "smtp.126.com";
        // 授权密码
        String passWord = "87477zhang";
        // 设置邮件发送相关属性
        Properties properties = System.getProperties();
        
        // 设置邮件传输采用的协议smtp(这里使用网易的smtp服务器)
        properties.setProperty("mail.transport.protocol", "smtp");
        //设置发送邮件的邮件服务器的属性
        properties.setProperty("mail.smtp.host", host);
        //需要经过授权,也就是有户名和密码的校验,这样才能通过验证(一定要有这一条)
        properties.setProperty("mail.smtp.auth", "true");
        
        // SMTP 服务器的端口 (非 SSL 连接的端口一般默认为 25, 可以不添加, 如果开启了 SSL 连接,
        // 需要改为对应邮箱的 SMTP 服务器的端口, 具体可查看对应邮箱服务的帮助,
        // QQ邮箱的SMTP(SLL)端口为465或587, 其他邮箱自行去查看)
        /*
        final String smtpPort = "465";
        properties.setProperty("mail.smtp.port", smtpPort);
        properties.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        properties.setProperty("mail.smtp.socketFactory.fallback", "false");
        properties.setProperty("mail.smtp.socketFactory.port", smtpPort);
        */
        
        // 获取默认的 Session 对象
        Session session = Session.getDefaultInstance(properties);
        // 会话采用debug模式
        session.setDebug(true);
        try {
            // 创建邮件对象
            MimeMessage message = new MimeMessage(session);
            // 设置发送邮件地址,param1 代表发送地址 param2 代表发送的名称(任意的) param3 代表名称编码方式
            message.setFrom(new InternetAddress(from, "发件人名称", "utf-8"));
            // 代表收件人
            message.setRecipient(Message.RecipientType.TO, new InternetAddress(to, "收件人名称", "utf-8"));
            // To: 增加更多收件人(可选)
            //message.addRecipient(MimeMessage.RecipientType.TO, new InternetAddress("chimuhuadao@126.com", "收件人名称", "UTF-8"));
            //message.addRecipient(MimeMessage.RecipientType.TO, new InternetAddress("chimuhuadao@163.com", "收件人名称", "UTF-8"));
            // Cc: 抄送(可选)
            //message.setRecipient(MimeMessage.RecipientType.CC, new InternetAddress("chimuhuadao@126.com", "抄送人名称", "UTF-8"));
            // Bcc: 密送(可选)
            //message.setRecipient(MimeMessage.RecipientType.BCC, new InternetAddress("chimuhuadao@126.com", "密送人名称", "UTF-8"));
            // 设置邮件主题
            message.setSubject("测试转发邮件");
            
            
            
            //====================附件测试开始=================
            //以下部分为测试邮件附件,如不需要可以把整段附件这部分代码注释
            
            // 创建 Multipart 对象,来包含邮件的多部分(正文,附件等)消息
            Multipart multipart = new MimeMultipart();
            
            // 第一部分正文消息部分 
            BodyPart bodyPart = new MimeBodyPart();
            // 设置邮件正文内容
            bodyPart.setContent("<h1>早安,世界</h1>", "text/html;charset=utf-8");
            // 将正文消息部分添加到 Multipart 对象中
            multipart.addBodyPart(bodyPart);
    
    
            // 第二部分是附件
            bodyPart = new MimeBodyPart();
            // 读取项目根目录下 upload 文件夹内 00125943U-0.jpg 文件作为附件,这里路径为 D:\ApacheServer\web_java\HelloWorld\WebContent\\upload\00125943U-0.jpg。这里WebContent为实际项目运行根目录
            String fileName = "00125943U-0.jpg";
            String filePath = request.getServletContext().getRealPath("./") + "upload" + File.separator + fileName;
            DataSource dataSource = new FileDataSource(filePath);
            bodyPart.setDataHandler(new DataHandler(dataSource));
            // 设置邮件中附件名称
            bodyPart.setFileName("testAttachment.jpg");
            // 将附件部分添加到 Multipart 对象中
            multipart.addBodyPart(bodyPart);
            //response.getWriter().append(filePath);if(true)return;
            
            // 另一份附件,可发送多个附件
            bodyPart = new MimeBodyPart();
            // 读取项目根目录下 upload 文件夹内 00125943U-0.jpg 文件作为附件
            fileName = "abc.ppt";
            filePath = request.getServletContext().getRealPath("./") + "upload" + File.separator + fileName;
            dataSource = new FileDataSource(filePath);
            bodyPart.setDataHandler(new DataHandler(dataSource));
            // 设置邮件中附件名称
            bodyPart.setFileName("testAttachment.ppt");
            // 将附件部分添加到 Multipart 对象中
            multipart.addBodyPart(bodyPart);
            
            
            //和下面发送文本的 message.setContent("<h1>早安,世界</h1>", "text/html;charset=utf-8"); 二选一执行
            message.setContent(multipart);
            //====================附件测试结束=================
            
            
            // 设置邮件内容,可以带HTML标签,也可以不带,内容大小不限
            //message.setContent("<h1>早安,世界</h1>", "text/html;charset=utf-8");
            // 设置发送时间
            message.setSentDate(new Date());
            // 保存上面的编辑内容
            message.saveChanges();
            
            Transport transport = session.getTransport();
            // 链接邮件服务器
            transport.connect(from, passWord);
            // 发送信息
            transport.sendMessage(message, message.getAllRecipients());
            // 关闭链接
            transport.close();
        }catch(Exception exception) {
            // 处理错误
            exception.printStackTrace();
        }
    }
    
    
    // 自建方法,用来处理 GET URL 或 application/x-www-form-urlencoded 方式的 POST
        protected void getOrFormUrlencoded(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
            // getParameter,getParameterNames 只能获取 get 参数(POST 请求里也能用 getParameter 获取 GET 参数)或者编码格式为 application/x-www-form-urlencoded 的 post 数据,无法获取 multipart/form-data 的 post 数据
            // application/x-www-form-urlencoded 传的 post 数据实际和 get 方式一样,都是格式化成 参数1=值1&参数2=值2&参数3=值3&... ,区别是 get 参数在 url 中,post 参数在请求体里。
            String name_val = request.getParameter("name");
            response.getWriter().append("<br/>表单名 : name 单一值为 : " + name_val);
            
            // getParameterNames() 方法获取所有提交的表单参数名。该方法返回一个枚举对象,包含未指定顺序的参数名
            Enumeration paramNames = request.getParameterNames();
            // hasMoreElements() 循环遍历判断该枚举是否有更多元素,指针每次往后移一位
            while(paramNames.hasMoreElements()) {
                // nextElement() 每次获取该枚举一个元素,指针每次往后移一位
                String paramName = (String)paramNames.nextElement();
                // 根据参数名获取对应参数值,但不知道该参数名对应单个值还是多个值(复选框即是多个表单同名,不同值,与PHP不同JAVA相同复选框名后面不用加中括号 [] ),所以用数组接收
                String paramValues[] = request.getParameterValues(paramName);
                // 读取参数值的数据
                if (paramValues.length == 1) { // 该参数名对应单一参数值
                    // 参数值为 paramValues[0]
                    String paramValue = paramValues[0];
                    if (paramValue.length() > 0) { // 判断该参数值有实际字符串内容
                        response.getWriter().append("<br/>表单名 : " + paramName + "单一值为 : " + paramValue);
                    }
                } else {
                    // 读取多个值的数据
                    for(int i=0; i < paramValues.length; i++) {
                        // 值为 paramValues[i]
                        response.getWriter().append("<br/>多重参数名 : " + paramName + "多重id值 : "+i+"为 : " + paramValues[i]);
                    }
                }
            }
        }
        // 自建方法,用来处理 multipart/form-data 方式的 POST,若不想方法名后面跟 throws 抛出异常,则需在方法内执行有可能抛出异常的语句时有 try catch 捕获异常
        protected void formData(HttpServletRequest request, HttpServletResponse response){
            try {
                // 创建磁盘文件项目工厂(form-data方式POST获取参数必须用的不管有无上传文件),可设置限制上传文件的临时存储目录等
                DiskFileItemFactory factory = new DiskFileItemFactory();
                
                // 可有可无,setSizeThreshold方法判断post数据(包括上传文件及表单数据)的大小(以字节为单位的int值,大于该值则post数据以临时文件形式存在磁盘,小于等于此值则存在内存中),如果从没有调用该方法设置此临界值,将会采用系统默认值10KB。对应的getSizeThreshold() 方法用来获取此临界值。
                //factory.setSizeThreshold(1024 * 1024 * 3); // 3MB
                
                // 可有可无,设置当上传数据大于 setSizeThreshold 设置的值时,临时文件在磁盘上的存放目录。当从没有此方法设置临时文件存储目录时,采用系统默认的临时文件路径 Tomcat 系统默认临时目录为 tomcat安装目录/temp/,默认的临时文件路径可通过 System.getProperty("java.io.tmpdir");查看
                //factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
                
                
                // 通过文件工厂对象创建上传对象,可设置 post 文件最大上传大小,请求数据最大值(包含文件和表单数据)等
                ServletFileUpload upload = new ServletFileUpload(factory);
                
                // 可有可无,设置上传的单个文件的最大字节数为100M
                //upload.setFileSizeMax(1024 * 1024 * 100);

                // 可有可无,设置整个表单的最大字节数为1G (包含文件和表单数据)
                //upload.setSizeMax(1024 * 1024 * 1024);
                
                // 可有可无,中文处理,解决上传数据的中文乱码
                //upload.setHeaderEncoding("UTF-8");
                
                // 构造路径存储上传的文件,request.getServletContext().getRealPath("./") 获取的路径 为 WEB-INF 文件夹父级目录,即项目根目录路径,这里路径为 D:\ApacheServer\web_java\HelloWorld\WebContent\\upload\00125943U-0.jpg。这里WebContent为实际项目运行根目录
                String uploadPath = request.getServletContext().getRealPath("./") + "upload";
                // 如果目录不存在则创建,目录同样只能一级一级创建,不能一次创建多级
                File uploadDir = new File( uploadPath );
                if (!uploadDir.exists()) {
                    uploadDir.mkdir();
                }
                
                // List类传泛型 FileItem 则其创建对象内元素都变为 FileItem 类型
                // 将用户请求传给设置好参数的上传对象,返回一个数组
                List<FileItem> item_list = upload.parseRequest(request);
                
                // 创建 param_map 保存提交表单的键值对
                Map param_map = new HashMap();
                // 假设有未知个数同表单名的复选框表单提交信息,用 testCheckBox 存该复选框选中的值
                Map testCheckBox = new HashMap();
                
                if (item_list != null && item_list.size() > 0) {
                    // 遍历上传的表单元素
                    for(FileItem fileItem : item_list){
                        // fileItem.getFieldName() 表单参数名
                        String fieldName = fileItem.getFieldName();
                        // fileItem.getString("UTF-8") 获取表单参数的值,或者上传文件的文本内容
                        String fieldVal  = fileItem.getString("UTF-8"); // 如果页面编码是 UTF-8 的
                        
                        // 处理在表单中的字段
                        if (fileItem.isFormField()) {
                            response.getWriter().append("<br/>表单参数名 : " + fieldName + " 参数值为 : " + fieldVal);
                            
                            // 当表单名为 testCheckBox 时,将其不确定个数的多个值存到 Map 对象中
                            if(fieldName.equals("testCheckBox")) {
                                testCheckBox.put(testCheckBox.size(), fieldVal);
                                
                            }
                            // 把post参数以键值对形式重新存到 param_map 列表中。复选框的话最后一个选中的同名值会覆盖之前的值
                            param_map.put(fieldName, fieldVal);
                        }
                        
                        // 处理表单中上传文件,这里上传文件的表单名任意都可上传
                        if (!fileItem.isFormField()) {
                            // ===============上传文件
                            // 获取上传的文件名(含扩展名),fileItem.getName()只能获取上传文件的完整名,不能获取其他post表单参数值
                            String fileName = fileItem.getName();
                            // 拼接将文件保存本地完整路径及文件+扩展名。File.separator 表示目录分割斜杠
                            String filePath = uploadPath + File.separator + fileName;
                            // 将合成的完整路径文件名 filePath 放入 File 对象中,File 对象代表磁盘中实际存在的文件和目录
                            File saveFile = new File(filePath);
                            // 限制上传文件扩展名
                            String suffix_limit[]   = {".html", ".jpg", ".jsp"};
                            SuffixFileFilter filter = new SuffixFileFilter(suffix_limit);
                            boolean flag = filter.accept(saveFile);
                            if(!flag) {
                                response.getWriter().append("<br/>上传文件类型不符,文件名为 : " + fileName);
                                return;
                            }
                            // 将该上传文件按指定路径保存文件到硬盘
                            fileItem.write(saveFile);
                            request.setAttribute("message", "文件上传成功!");
                            
                        }
                    }
                }
                // 可以使用 param_map.get 获取表单参数值了
                String name_val = (String) param_map.get("name");
                
                response.getWriter().append("<br/>表单name的值 : " + name_val);
                
                // 遍历获取复选框的 Map 对象里每个值
                for(int i = 0;i<testCheckBox.size();i++) {
                    response.getWriter().append("<br/>复选框i : " + i + "对应值 : " + (String) testCheckBox.get(i));
                }
                
            } catch (FileUploadException fex) {
                fex.printStackTrace();
            } catch (Exception ex) {
                request.setAttribute("message", "错误信息: " + ex.getMessage());
            }
        }
    
}

TestFilter.java

// 文件路径 D:\ApacheServer\web_java\HelloWorld\src\com\test\TestFilter.java
package com.test;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;


// 浏览器暂不能通过 http://localhost:8080/HelloWorld/TestFilter 访问页面
@WebFilter("/TestFilter")
//实现 Filter 类
public class TestFilter implements Filter {

    
    public TestFilter() {
        
    }

    // Servlet容器在销毁 Filter 实例前调用该方法,在该方法中释放 Filter 实例占用的资源。
    public void destroy() {
        // TODO Auto-generated method stub
    }

    // 该方法完成实际的过滤操作,当客户端请求过滤器设置的 URL 时,Servlet 容器将先调用过滤器的 doFilter 方法。FilterChain 用户访问后续过滤器。
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 这里的 request 和 response 对象与 Servlet 类中一样可获取用户请求信息及直接返回响应信息

        // pass the request along the filter chain
        // 把请求传回过滤链
        chain.doFilter(request, response);
    }

    // web 容器启动时,web 服务器将创建 Filter 的实例对象,并调用其 init 方法,读取 web.xml 配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作( filter 对象只会创建一次,init 方法也只会执行一次)。开发人员通过 init 方法的参数,可获得代表当前 filter 配置信息的 FilterConfig 对象。
    public void init(FilterConfig fConfig) throws ServletException {
        // 获取初始化参数
        String testParam = fConfig.getInitParameter("testParam"); 
        // 输出初始化参数
        System.out.println("web.xml 配置 测试参数 testParam 值为 : " + testParam);
    }

}
// Console 信息界面会出现 web.xml 配置 测试参数 testParam 值为 : 测试配置参数值 信息

TestErrorServlet.java

// 文件路径 D:\ApacheServer\web_java\HelloWorld\src\com\test\TestErrorServlet.java
package com.test;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


@WebServlet("/TestErrorServlet")
public class TestErrorServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
       

    public TestErrorServlet() {
        super();
    }

    
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        
        //response.getWriter().append("Served at: ").append(request.getContextPath());
        
        // javax.servlet.error.status_code 该属性给出状态码,状态码可被存储,并在存储为 java.lang.Integer 数据类型后可被分析
        Integer statusCode  = (Integer) request.getAttribute("javax.servlet.error.status_code");
        
        // javax.servlet.error.request_uri 该属性给出报错页面的请求地址,可被存储,并在存储为 java.lang.String 数据类型后可被分析
        String requestUri = (String) request.getAttribute("javax.servlet.error.request_uri");
        
        // javax.servlet.error.message 该属性给出确切错误消息信息,信息可被存储,并在存储为 java.lang.String 数据类型后可被分析
        String message  = (String) request.getAttribute("javax.servlet.error.message");
        
        // javax.servlet.error.servlet_name 该属性给出报错的 Servlet 类名,可被存储,并在存储为 java.lang.String 数据类型后可被分析
        String servletName  = (String) request.getAttribute("javax.servlet.error.servlet_name");
        
        // javax.servlet.error.exception_type 该属性给出异常的类型,异常类型可被存储,并在存储为 java.lang.Class 数据类型后可被分析
        Object exceptionType  = request.getAttribute("javax.servlet.error.exception_type");
        
        // javax.servlet.error.exception 该属性给出异常的相关信息,信息可被存储,并在存储为 java.lang.Throwable 数据类型后可被分析
        Throwable exception = (Throwable) request.getAttribute("javax.servlet.error.exception");
        
        
        
        
        
        
        // 设置返回响应内容类型及编码
        response.setContentType("text/html;charset=UTF-8");
        // 以下项是抛出异常或报 404 等错误码时均有返回信息
        response.getWriter().append("<br/> 访问报错 Servlet 页面返回的错误码 : " + statusCode + " 这里显示内容为 404 或 500 ");
        response.getWriter().append("<br/> 访问报错 Servlet 页面的请求地址 : " + requestUri + " 这里显示内容为 /HelloWorld/TomcatTest/TestServlet ");
        response.getWriter().append("<br/> 访问报错 Servlet 页面返回的错误信息 : " + message + " 404 错误时这里显示内容为 test response status 异常错误时显示内容为 这是测试异常信息");
        response.getWriter().append("<br/> 访问报错 Servlet 页面返回的报错的 Servlet 类名 : " + servletName + " 这里显示内容为 TestServlet  ");
        
        // 以下项是只有抛出异常才有值,报 404 等错误码时无返回值
        response.getWriter().append("<br/> 访问报错 Servlet 页面返回的异常类型 : " + exceptionType.toString() + " 这里显示内容为 class javax.servlet.ServletException ");
        response.getWriter().append("<br/> 访问报错 Servlet 页面返回的异常类型 : " + exception.getClass().getName() + " 这里显示内容为 class javax.servlet.ServletException ");
        response.getWriter().append("<br/> 访问报错 Servlet 页面返回的异常信息 : " + exception.getMessage() + " 这里显示内容为 这是测试异常信息 ");
        
        
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

}

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- 文件路径 D:\ApacheServer\web_java\HelloWorld\WebContent\WEB-INF\web.xml -->
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">



  <!-- 注册一个过滤器 -->
  <filter>
    <!-- 注册一个过滤器名称 -->
    <filter-name>TestFilter</filter-name>
    <!-- 该注册名对应的实际 Filter 类的完整的包及类名 -->
    <filter-class>com.test.TestFilter</filter-class>
    <!-- 为过滤器指定初始化参数,它的子元素 <param-name> 指定参数的名字,<param-value> 指定参数的值 -->
    <init-param>
      <param-name>testParam</param-name>
      <param-value>测试配置参数值</param-value>
    </init-param>
  </filter>
  
  <!-- 方便测试,再注册一个 filter -->
  <filter>
    <filter-name>TestFilter2</filter-name>
    <filter-class>com.test.TestFilter</filter-class>
    <init-param>
      <param-name>testParam2</param-name>
      <param-value>测试配置参数值2</param-value>
    </init-param>
  </filter>
  
  <!-- web.xml 中的 filter-mapping 元素的顺序决定了某个请求时 Web 容器调用 filter 的顺序 -->
  <!-- <filter-mapping> 元素用于设置一个 Filter 所负责拦截的资源。一个 Filter 拦截的资源可通过两种方式来指定:资源访问的请求路径和 Servlet 名称 -->
  <filter-mapping>
    <!-- 设置负责此次过滤功能的 Filter 的注册名称即该值必须是在<filter>元素中声明过的过滤器的名字 -->
    <filter-name>TestFilter</filter-name>
    <!-- 设置该 Filter 所拦截的请求路径。此处的 /* 表示该过滤器适用于所有的 Servlet和请求路径 -->
    <url-pattern>/*</url-pattern>
    <!-- 也可以指定特定的 Servlet 注册名,在访问指定 Servlet 上应用该过滤器 -->
    <servlet-name>TestServlet</servlet-name>
    <!-- dispatcher 访问指定资源时,调用该过滤器的条件,可以是 REQUEST,,INCLUDE,,FORWARD 和 ERROR 之一,默认 REQUEST。用户可以设置一个或多个 <dispatcher> 子元素用来指定 Filter 对资源的多种调用方式进行拦截。该参数可不写 -->
    <dispatcher>REQUEST</dispatcher> <!-- 当用户直接访问时,Web 容器将会调用过滤器。如果目标资源是通过 RequestDispatcher 的 include() 或 forward() 方法访问时,那么该过滤器就不会被调用 -->
    <dispatcher>INCLUDE</dispatcher> <!-- 如果目标资源是通过 RequestDispatcher 的 include() 方法访问时,那么该过滤器将被调用。除此之外,该过滤器不会被调用 -->
    <dispatcher>FORWARD</dispatcher> <!-- 如果目标资源是通过 RequestDispatcher 的 forward() 方法访问时,那么该过滤器将被调用,除此之外,该过滤器不会被调用 -->
    <dispatcher>ERROR</dispatcher>   <!-- 如果目标资源是通过声明式异常处理机制调用时,那么该过滤器将被调用。除此之外,过滤器不会被调用 -->
  </filter-mapping>
  
  <!-- 如果拦截的请求相同,则先执行上一个 filter-mapping 中指定的 Filter -->
  <filter-mapping>
    <filter-name>TestFilter2</filter-name>
    <url-pattern>/TomcatTest/TestServlet</url-pattern>
  </filter-mapping>





  <servlet>
    <!-- Servlet 在此 xml 里的注册名 -->
    <servlet-name>TestServlet</servlet-name>
    <!-- 该注册名对应的实际 Servlet 类的完整包名类名 -->
    <servlet-class>com.test.TestServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <!-- 指定一个 Servlet 注册名 -->
    <servlet-name>TestServlet</servlet-name>
    <!-- 外部访问的网址 -->
    <url-pattern>/TomcatTest/TestServlet</url-pattern>
  </servlet-mapping>
  
  <servlet>
    <servlet-name>TestErrorServlet</servlet-name>
    <servlet-class>com.test.TestErrorServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>TestErrorServlet</servlet-name>
    <url-pattern>/TomcatTest/TestErrorServlet</url-pattern>
  </servlet-mapping>
  
  
  

  
  <!-- 当请求的路径报错时指定一个 Servlet 来处理及返回相应信息,但用户请求的 url 未重定向跳转 -->
  <error-page>
    <!-- 当客户端请求 web 容器返回指定错误状态代码时(示例是404,也可是403等)调用指定的 Servlet 页面 -->
    <error-code>404</error-code>
    <location>/TomcatTest/TestErrorServlet</location>
  </error-page>
  
  <error-page>
    <!-- 当客户端请求 web 容器抛出异常时调用指定的 Servlet 页面,示例的 java.lang.Throwable 对应所有web容器抛出的异常,也可换成 javax.servlet.ServletException 或 java.io.IOException 等抛出指定异常时才调用设置的 Servlet 页面 -->
    <exception-type>java.lang.Throwable</exception-type >
    <location>/TomcatTest/TestErrorServlet</location>
  </error-page>
  
  <!-- 设置 session 超时时间,单位分钟,该设置将覆盖 Tomcat 默认的 30 分钟超时时间 -->
  <session-config>
    <session-timeout>15</session-timeout>
  </session-config>
  
  
</web-app>

 

Servlet

标签:邮件服务   地址   throwable   fresh   刷新   simple   serial   rom   field   

原文地址:https://www.cnblogs.com/dreamhome/p/11397444.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!