IntelliJ IDEA 生成 serialVersionUID

serialVersionUID的作用

详见这篇文档  http://lenjey.iteye.com/blog/513736

简单来说就是,在序列化对象时,编译器会自动给对象生成一个serialVersionUID,之后任意改动这个对象都会与编译器生成的UID不匹配。如果我们显示指定了serialVersionUID,那么对象序列化之后,也可以修改这个对象。

serialVersionUID生成

我们可以任意指定serialVersionUID,如

private static final long serialVersionUID = 1L;

但最好还是使用让系统按照摘要算法生成一个指纹数字。eclipse是可以自动生成的,IDEA需要简单配置一下。

IntelliJ IDEA->Preferences->Inspections界面下

Serializable class without serialVersionUID后打钩,注意上面的Profile选为Default。

(继承了Serializable的类没有serialVersionUID就会Warning)

之后光标在继承了Serializable的类名上按 ALT+ENTER键,就会出现 Add serialVersionUID field 选项,生成的UID如下

private static final long serialVersionUID = 6496849704004740865L;

在Windows 7系统上编译运行体验Apache OFBiz-13.07.03

作为Apache基金会的赞助项目,OFBiz(全称为"Open for Business")是一套功能齐全的企业自动化套件,其中包含企业资源规划(简称ERP)、客户关系管理(简称CRM)、电子商务、内容管理、计费与费 率管理、供应链管理、制造资源规划以及企业资产管理等方案。OFBiz拥有丰富的说明文档及指导视频,其基于Java语言因此能够运行在任意支持Java SDK的系统当中,包括WindowsOS XLinux以及Unix

1.去OFBiz的官网http://ofbiz.apache.org/下载目前最新版本的13.07.03。下载完成后,是个zip格式的压缩包。解压压缩包到任意目录。

2.安装最新的JDK(要求最低是1.7,目前的1.8版本是可以正常使用的)。

$ cd apache-ofbiz-13.07.03
$ ant build
$ ant load-demo
$ ant start

完成后,直接访问http://localhost:8080/ecommerce即可看到如下页面:
ofbiz_index
其他的参考代码根目录下面的README.

参考链接


IntelliJ IDEA 2016.2使用Spring 4.3.1.RELEASE,sockjs-1.1.1,stomp-1.2搭建基于Tomcat-7.0.68的WebSocket应用

接着上文IntelliJ IDEA 2016.2使用Spring 4.3.1.RELEASE搭建基于Tomcat-7.0.68的WebSocket应用

上文的最后我们说到,WebSocket是需要定时心跳的,否则会在一段时间后自动断开连接,而更重要的是,不是所有的浏览器都支持WebSocket,早期的IE 10之前的版本就是不支持的,而这一部分的设备其实是不算少的,而sockjs的出现,恰恰好来解决了这个问题。对于不支持WebSocket的浏览器,sockjs使用了多种方式来兼容这种情况,包括使用长轮询等方式,Spring更是内建支持这种方式。

下面我们看如何在上篇文章的基础上,增加对于sockjs的支持。

首先是STOMP的文档官网地址 http://stomp.github.io/
代码的地址为https://github.com/jmesnil/stomp-websocket,项目下面的lib/stomp.js就是我们想要的文件。也可以本站下载stomp.js.zip

接下来sockjs的代码地址https://github.com/sockjs/sockjs-client,项目下面的dist/sockjs-1.1.1.js就是我们想要的文件。也可以本站下载sockjs-1.1.1.js.zip

接下来我们把下载到的文件放到我们工程目录下面的web->resources->javascript目录下面,如下图:

stomp-websockjs-resources

接下来,添加我们需要的com.fasterxml.jackson.core:jackson-annotations:2.8.1,com.fasterxml.jackson.core:jackson-core:2.8.1,com.fasterxml.jackson.core:jackson-databind:2.8.1这三个jar包,增加的方式参照上一篇中对于javax.servlet:javax.servlet-api:3.1.0的操作方法。与上一篇的操作不同的是,这次添加的三个jar包,都要放到编译完成后的War包中。最后的结果如下图:
ToolsJacksonMaven

ToolsJacksonMavenWar

下面,我们开始进行代码的操作,我们在上篇文章中的src->Tools->WebSocket中新增两个源代码文件SockJsController.java,WebJsSocketConfig.java.如下图:

NewJavaSourcesForSockjs

其中的代码如下:
SockJsController.java

package Tools.WebSocket;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class SockJsController {

        @MessageMapping("/hello")
        @SendTo("/hello/subscribe") /*貌似这个名字可以随意的,主要用在stomp.subscribe时候的名字*/
        public String Hello(String message) throws Exception {
            return new String("Hello");
        }
}

WebJsSocketConfig.java

package Tools.WebSocket;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;


@Configuration
@EnableWebSocketMessageBroker
public class WebJsSocketConfig  extends AbstractWebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/webSocketServer");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/hello").setAllowedOrigins("*").withSockJS();
    }
}

然后修改WebSocket.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>WebSocket/SockJS Echo Sample (Adapted from Tomcat's echo sample)</title>
    <style type="text/css">
        #connect-container {
            float: left;
            width: 400px
        }

        #connect-container div {
            padding: 5px;
        }

        #console-container {
            float: left;
            margin-left: 15px;
            width: 400px;
        }

        #console {
            border: 1px solid #CCCCCC;
            border-right-color: #999999;
            border-bottom-color: #999999;
            height: 170px;
            overflow-y: scroll;
            padding: 5px;
            width: 100%;
        }

        #console p {
            padding: 0;
            margin: 0;
        }
    </style>

    <script src="./resources/javascript/sockjs-1.1.1.js"></script>
    <script src="./resources/javascript/stomp.js"></script>

    <script type="text/javascript">
        var ws = null;
        var url = null;
        var transports = [];
        var stompClient = null;

        function setConnected(connected) {
            document.getElementById('connect').disabled = connected;
            document.getElementById('disconnect').disabled = !connected;
            document.getElementById('echo').disabled = !connected;
        }

        function connect() {
            if (!url) {
                alert('Select whether to use W3C WebSocket or SockJS');
                return;
            }

            ws = (url.indexOf('sockjs') != -1) ?
                    new SockJS(url,undefined, {transports: transports}) : new WebSocket(url);
            if((url.indexOf('sockjs') != -1)) {
                stompClient = Stomp.over(ws);
                stompClient.connect({}, function(frame) {
                    setConnected(true);
                    log('Connected: ' + frame);
                    stompClient.subscribe('/hello/subscribe', function(message){
                        log(message.body);
                    });
                });
            }else {
                ws.onopen = function () {
                    setConnected(true);
                    log('Info: connection opened.');
                };

                ws.onmessage = function (event) {
                    log('Received: ' + event.data);
                };

                ws.onclose = function (event) {
                    setConnected(false);
                    log('Info: connection closed.');
                    log(event);
                };
            }
        }

        function disconnect() {
            if (ws != null) {
                ws.close();
                ws = null;
            }
            stompClient = null;
            setConnected(false);
        }

        function echo() {
            if(stompClient != null){
                var message = document.getElementById('message').value;
                stompClient.send("/webSocketServer/hello", {}, message);
                log('Sent: ' + message);
            }else {
                if (ws != null) {
                    var message = document.getElementById('message').value;
                    log('Sent: ' + message);
                    ws.send(message);
                } else {
                    alert('connection not established, please connect.');
                }
            }
        }

        function updateUrl(urlPath) {
            if (urlPath.indexOf('sockjs') != -1) {
                url = urlPath;
                document.getElementById('sockJsTransportSelect').style.visibility = 'visible';
            }
            else {
                if (window.location.protocol == 'http:') {
                    url = 'ws://' + window.location.host + urlPath;
                } else {
                    url = 'wss://' + window.location.host + urlPath;
                }
                document.getElementById('sockJsTransportSelect').style.visibility = 'hidden';
            }
        }

        function updateTransport(transport) {
            transports = (transport == 'all') ?  [] : [transport];
        }

        function log(message) {
            var console = document.getElementById('console');
            var p = document.createElement('p');
            p.style.wordWrap = 'break-word';
            p.appendChild(document.createTextNode(message));
            console.appendChild(p);
            while (console.childNodes.length > 25) {
                console.removeChild(console.firstChild);
            }
            console.scrollTop = console.scrollHeight;
        }
    </script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websockets
    rely on Javascript being enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div>
    <div id="connect-container">
        <input id="radio1" type="radio" name="group1" onclick="updateUrl('/webSocketServer');">
        <label for="radio1">W3C WebSocket</label>
        <br>
        <input id="radio2" type="radio" name="group1" onclick="updateUrl('/webSocketServer/sockjs/hello');">
        <label for="radio2">SockJS</label>
        <div id="sockJsTransportSelect" style="visibility:hidden;">
            <span>SockJS transport:</span>
            <select onchange="updateTransport(this.value)">
                <option value="all">all</option>
                <option value="websocket">websocket</option>
                <option value="xhr-polling">xhr-polling</option>
                <option value="jsonp-polling">jsonp-polling</option>
                <option value="xhr-streaming">xhr-streaming</option>
                <option value="iframe-eventsource">iframe-eventsource</option>
                <option value="iframe-htmlfile">iframe-htmlfile</option>
            </select>
        </div>
        <div>
            <button id="connect" onclick="connect();">Connect</button>
            <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
        </div>
        <div>
            <textarea id="message" style="width: 350px">Here is a message!</textarea>
        </div>
        <div>
            <button id="echo" onclick="echo();" disabled="disabled">Echo message</button>
        </div>
    </div>
    <div id="console-container">
        <div id="console"></div>
    </div>
</div>

</body>
</html>

最后,我们修改web->WEB-INF->web.xml,在其中增加

<servlet-mapping>
	<servlet-name>dispatcher</servlet-name>
	<url-pattern>/webSocketServer/sockjs/*</url-pattern>
</servlet-mapping>

修改后的最终结果如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <filter>
        <filter-name>struts2</filter-name>
        <filter-class>Tools.Filter.StrutsPrepareAndExecuteFilterEx</filter-class>
        <async-supported>true</async-supported>
    </filter>
    <filter-mapping>
        <filter-name>struts2</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/webSocketServer/*</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/webSocketServer/sockjs/*</url-pattern>
    </servlet-mapping>
</web-app>

参考链接


 

Ubuntu 16.04下Tomcat 7.0.68启动服务时候报告“java.lang.NoSuchMethodError: java.util.concurrent.ConcurrentHashMap.keySet()Ljava/util/concurrent/ConcurrentHashMap$KeySetView;”

在使用Ubuntu16.04 安装openjdk-7-jdk介绍的方式切换Java1.7版本后,在进行代码调试的时候,Tomcat 7.0.68报告如下错误:

java.lang.NoSuchMethodError: java.util.concurrent.ConcurrentHashMap.keySet()Ljava/util/concurrent/ConcurrentHashMap$KeySetView;
at org.apache.catalina.core.ApplicationContext.getInitParameterNames(ApplicationContext.java:368)
at org.apache.catalina.core.ApplicationContextFacade.getInitParameterNames(ApplicationContextFacade.java:367)
at org.springframework.web.context.support.WebApplicationContextUtils.registerEnvironmentBeans(WebApplicationContextUtils.java:229)
at org.springframework.web.context.support.AbstractRefreshableWebApplicationContext.postProcessBeanFactory(AbstractRefreshableWebApplicationContext.java:165)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:520)
at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:444)
at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:326)
at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:107)
at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:5068)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5584)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:147)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:899)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:875)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:652)
at org.apache.catalina.startup.HostConfig.manageApp(HostConfig.java:1859)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.apache.tomcat.util.modeler.BaseModelMBean.invoke(BaseModelMBean.java:301)
at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.invoke(DefaultMBeanServerInterceptor.java:819)
at com.sun.jmx.mbeanserver.JmxMBeanServer.invoke(JmxMBeanServer.java:801)
at org.apache.catalina.mbeans.MBeanFactory.createStandardContext(MBeanFactory.java:618)
at org.apache.catalina.mbeans.MBeanFactory.createStandardContext(MBeanFactory.java:565)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.apache.tomcat.util.modeler.BaseModelMBean.invoke(BaseModelMBean.java:301)
at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.invoke(DefaultMBeanServerInterceptor.java:819)
at com.sun.jmx.mbeanserver.JmxMBeanServer.invoke(JmxMBeanServer.java:801)
at javax.management.remote.rmi.RMIConnectionImpl.doOperation(RMIConnectionImpl.java:1487)
at javax.management.remote.rmi.RMIConnectionImpl.access$300(RMIConnectionImpl.java:97)
at javax.management.remote.rmi.RMIConnectionImpl$PrivilegedOperation.run(RMIConnectionImpl.java:1328)
at javax.management.remote.rmi.RMIConnectionImpl.doPrivilegedOperation(RMIConnectionImpl.java:1420)
at javax.management.remote.rmi.RMIConnectionImpl.invoke(RMIConnectionImpl.java:848)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:322)
at sun.rmi.transport.Transport$2.run(Transport.java:202)
at sun.rmi.transport.Transport$2.run(Transport.java:199)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:198)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:567)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.access$400(TCPTransport.java:619)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:684)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:681)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:681)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:745)

这说明Ubuntu 16.04下的Tomcat是在Java 8环境下面编译的,尽管我们已经切换到Java 7下面了,但是Tomcat并不能很好的执行我们的Java 7代码。这个时候的解决方法就是,编译代码的时候指定Java 7,但是在Tomcat执行的时候,指定使用Java 8来运行。

参考链接


Java error java.util.concurrent.ConcurrentHashMap.keySet

IntelliJ IDEA 2016.2使用Spring 4.3.1.RELEASE搭建基于Tomcat-7.0.68的WebSocket应用

先参照 IntelliJ IDEA 2016.1建立Strut2工程并使用Tomcat调试建立新的工程,一步一步操作,包括最后引用Spring框架部分。

经过上面的操作,Spring-WebSocket的包应该已经被默认引入了,如下图所示:

spring-websocket-release-lib

这就意味着我们已经不需要再进行过多的额外配置了。

接下来,我们在src->Tools下面新建一个WebSocket的目录,里面创建三个Java文件。如下图:
WebSocketJavaSourceList

每个文件中的代码如下:
SystemWebSocketHandler.java

package Tools.WebSocket;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;

import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

public class SystemWebSocketHandler extends TextWebSocketHandler {

    private static final ArrayList<WebSocketSession> users = new ArrayList<>();


    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        System.out.println("ConnectionEstablished");
        users.add(session);

        session.sendMessage(new TextMessage("connect"));
        session.sendMessage(new TextMessage("new_msg"));

    }

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        System.out.println("handleMessage" + message.toString());
        session.sendMessage(new TextMessage(new Date() + ""));
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        if(session.isOpen()){
            session.close();
        }
        users.remove(session);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
        users.remove(session);
    }

    @Override
    public boolean supportsPartialMessages() {
        return false;
    }

    /**
     * 给所有在线用户发送消息
     *
     * @param message
     */
    public void sendMessageToUsers(TextMessage message) {
        for (WebSocketSession user : users) {
            try {
                if (user.isOpen()) {
                    user.sendMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

WebSocketConfig.java

package Tools.WebSocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebMvc
@EnableWebSocket
public class WebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(systemWebSocketHandler(), "/webSocketServer").addInterceptors(new WebSocketHandshakeInterceptor());
    }

    @Bean
    public WebSocketHandler systemWebSocketHandler() {
        return new SystemWebSocketHandler();
    }
}

WebSocketHandshakeInterceptor.java

package Tools.WebSocket;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import java.util.Map;

public class WebSocketHandshakeInterceptor implements HandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
        System.out.println("beforeHandshake");
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
        System.out.println("afterHandshake");
    }
}

由于我们使用了Struts2导致我们的网络请求优先被Struts2的拦截器拦截,而Struts2又处理不了websocket请求,结果直接返回了404错误。因此我们需要替换掉默认的在web.xml中定义的Struts2的拦截器,要求Struts2不处理websocket请求。
我们在src->Tools下面新建一个Filter的目录,下面创建一个名为StrutsPrepareAndExecuteFilterEx.java的源代码文件,如下图:

StrutsPrepareAndExecuteFilterEx
具体的代码如下:
StrutsPrepareAndExecuteFilterEx.java

package Tools.Filter;

import org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class StrutsPrepareAndExecuteFilterEx extends StrutsPrepareAndExecuteFilter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp,
                         FilterChain filter) throws IOException, ServletException {
        
        String requestURI = ((HttpServletRequest) req).getRequestURI();
        if(requestURI != null && (requestURI.contains("/webSocketServer")) || requestURI.startsWith("ws://") || requestURI.contains("/mspjapi"))
            filter.doFilter(req, resp);
        else
            super.doFilter(req, resp, filter);
    }
}

这时候的代码是无法编译通过的,原因是依赖的javaxJar包不存在。此时,我们需要手工引入javax.servlet:javax.servlet-api:3.1.0这个Jar包。如下图:

ToolWebSocketProjectStructure

ToolWebSocketProjectStructureAddJars

ToolWebSocketProjectStructureAddJarsFromMaven

ToolWebSocketProjectStructureAddJarsFromMavenJavaxServerletApi3.1.0

ToolWebSocketProjectStructureAddJarsFromMavenJavaxServerletApi3.1.0Add

ToolWebSocketProjectStructureAddJarsFromMavenJavaxServerletApi3.1.0AddApply

还要注意,刚刚添加进入的javax.servlet:javax.servlet-api:3.1.0这个Jar包,我们只在编译期间使用,在运行时候,使用Tomcat自己实现的那个同名Jar包。也就是这个包是个Provided,而不是Compile关系,具体如下图:

ToolWebSocketProjectStructureKeepOutsideWar

接下来,修改web.xml指定Struts2的拦截器为我们定义的拦截器

<filter>
	<filter-name>struts2</filter-name>
	<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
	<filter-name>struts2</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

修改为

<filter>
	<filter-name>struts2</filter-name>
	<filter-class>Tools.Filter.StrutsPrepareAndExecuteFilterEx</filter-class>
</filter>
<filter-mapping>
	<filter-name>struts2</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

经过上面的修改后,依旧是没办法进行网络访问的,原因是web.xml中的Spring拦截器并没有拦截WebSocket的数据请求。

<servlet>
	<servlet-name>dispatcher</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<load-on-startup>1</load-on-startup>
	<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
	<servlet-name>dispatcher</servlet-name>
	<url-pattern>*.form</url-pattern>
</servlet-mapping>

修改为

<servlet>
	<servlet-name>dispatcher</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<load-on-startup>1</load-on-startup>
	<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
	<servlet-name>dispatcher</servlet-name>
	<url-pattern>*.form</url-pattern>
</servlet-mapping>
<servlet-mapping>
	<servlet-name>dispatcher</servlet-name>
	<url-pattern>/webSocketServer/*</url-pattern>
</servlet-mapping>

注意增加的

<async-supported>true</async-supported>

下一步,配置Spring的配置文件web->WEB-INF->dispatcher-servlet.xml增加配置信息类的扫描目录包含我们刚刚创建的src->Tools->WebSocket的目录(缺少这一步会导致我们通过注解实现的配置信息类没有被自动加载,导致无法访问),修改后的内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- Scan for spring annotated components -->
    <context:component-scan base-package="Tools.WebSocket"/>
</beans>

最后,调用的页面的代码
WebSocket.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>WebSocket</title>
  <style type="text/css">

    #console-container {
      margin-left: 15px;
      margin-right: 15px;
      padding: 5px;
      width: 95%;
    }

    #console {
      border: 1px solid #CCCCCC;
      border-right-color: #999999;
      border-bottom-color: #999999;
      height: 300px;
      overflow-y: scroll;
      padding: 5px;
      width: 100%;
    }

    #console p {
      padding: 0;
      margin: 0;
    }
  </style>

  <script type="text/javascript">
    var url = 'ws://' + window.location.host + '/webSocketServer';
    var ws = new WebSocket(url);

    ws.onopen = function () {
      log('Info: connection opened.');
    };

    ws.onmessage = function (event) {
      log('Received: ' + event.data);
    };

    ws.onclose = function (event) {
      log('Info: connection closed.');
      log(event);
    };

    function log(message) {
      var console = document.getElementById('console');
      var p = document.createElement('p');
      p.style.wordWrap = 'break-word';
      p.appendChild(document.createTextNode(message));
      console.appendChild(p);
      while (console.childNodes.length > 1000) {
        console.removeChild(console.firstChild);
      }
      console.scrollTop = console.scrollHeight;
    }
  </script>
</head>
<body>

<div>
  <div id="console-container">
    <div id="console"></div>
  </div>
</div>

</body>
</html>

最后的客户端显示效果如下图所示:
WebSocketJsp

注意:如此创建的WebSocket是会在两三分钟后主动断开连接的,原因在于WebSocket需要周期性的发送心跳报文来维持连接。后续我们会尝试使用sockjs来实现自动发送心跳的逻辑。

具体的sockjs的接入方法,参考IntelliJ IDEA 2016.2使用Spring 4.3.1.RELEASE,sockjs-1.1.1,stomp-1.2搭建基于Tomcat-7.0.68的WebSocket应用

Java 读写Properties配置文件

1.Properties类与Properties配置文件

Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存属性集。不过Properties有特殊的地方,就是它的键和值都是字符串类型。

2.Properties中的主要方法

(1)load(InputStream inStream)

   这个方法可以从.properties属性文件对应的文件输入流中,加载属性列表到Properties类对象如下面的代码:

Properties pro = new Properties();
FileInputStream in = new FileInputStream("a.properties");
pro.load(in);
in.close();

(2)store(OutputStream out, String comments)

这个方法将Properties类对象的属性列表保存到输出流中如下面的代码:

FileOutputStream oFile = new FileOutputStream(file, "a.properties");
pro.store(oFile, "Comment");
oFile.close();

如果comments不为空,保存后的属性文件第一行会是#comments,表示注释信息;如果为空则没有注释信息。

注释信息后面是属性文件的当前保存时间信息。

(3)getProperty/setProperty

这两个方法是分别是获取和设置属性信息。

3.代码实例

属性文件a.properties如下:

name=root
pass=liu
key=value

读取a.properties属性列表,与生成属性文件b.properties。代码如下:

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream; 
import java.util.Iterator;
import java.util.Properties; 

public class PropertyTest {
    public static void main(String[] args) { 
        Properties prop = new Properties();     
        try{
            //读取属性文件a.properties
            InputStream in = new BufferedInputStream (new FileInputStream("a.properties"));
            prop.load(in);     ///加载属性列表
            Iterator<String> it=prop.stringPropertyNames().iterator();
            while(it.hasNext()){
                String key=it.next();
                System.out.println(key+":"+prop.getProperty(key));
            }
            in.close();
            
            ///保存属性到b.properties文件
            FileOutputStream oFile = new FileOutputStream("b.properties", true);//true表示追加打开
            prop.setProperty("phone", "10086");
            prop.store(oFile, "The New properties file");
            oFile.close();
        }
        catch(Exception e){
            System.out.println(e);
        }
    } 
}

参考链接


Java 读写Properties配置文件

在JNI编程中避免内存泄漏

JAVA中的内存泄漏

JAVA编程中的内存泄漏,从泄漏的内存位置角度可以分为两种:JVMJava Heap的内存泄漏;JVM内存中native memory的内存泄漏。

Java Heap的内存泄漏

Java对象存储在JVM进程空间中的Java Heap中,Java Heap可以在JVM运行过程中动态变化。如果Java对象越来越多,占据Java Heap的空间也越来越大,JVM会在运行时扩充Java Heap的容量。如果Java Heap容量扩充到上限,并且在GC后仍然没有足够空间分配新的Java对象,便会抛出out of memory异常,导致JVM进程崩溃。

Java Heapout of memory异常的出现有两种原因——①程序过于庞大,致使过多Java对象的同时存在;②程序编写的错误导致Java Heap内存泄漏。

多种原因可能导致Java Heap内存泄漏。JNI编程错误也可能导致Java Heap的内存泄漏。

JVMnative memory的内存泄漏

从操作系统角度看,JVM在运行时和其它进程没有本质区别。在系统级别上,它们具有同样的调度机制,同样的内存分配方式,同样的内存格局。

JVM进程空间中,Java Heap以外的内存空间称为JVMnative memory。进程的很多资源都是存储在JVMnative memory中,例如载入的代码映像,线程的堆栈,线程的管理控制块,JVM的静态数据、全局数据等等。也包括JNI程序中native code分配到的资源。

JVM运行中,多数进程资源从native memory中动态分配。当越来越多的资源在native memory中分配,占据越来越多native memory空间并且达到native memory上限时,JVM会抛出异常,使JVM进程异常退出。而此时Java Heap往往还没有达到上限。

多种原因可能导致JVMnative memory内存泄漏。例如JVM在运行中过多的线程被创建,并且在同时运行。JVM为线程分配的资源就可能耗尽native memory的容量。

JNI编程错误也可能导致native memory的内存泄漏。对这个话题的讨论是本文的重点。


JNI编程中明显的内存泄漏

JNI编程实现了native codeJava程序的交互,因此JNI代码编程既遵循 native code编程语言的编程规则,同时也遵守JNI编程的文档规范。在内存管理方面,native code编程语言本身的内存管理机制依然要遵循,同时也要考虑JNI编程的内存管理。

本章简单概括JNI编程中显而易见的内存泄漏。从native code编程语言自身的内存管理,和JNI规范附加的内存管理两方面进行阐述。

Native Code本身的内存泄漏

JNI编程首先是一门具体的编程语言,或者C语言,或者C++,或者汇编,或者其它native的编程语言。每门编程语言环境都实现了自身的内存管理机制。因此,JNI程序开发者要遵循native语言本身的内存管理机制,避免造成内存泄漏。以C语言为例,当用malloc()在进程堆中动态分配内存时,JNI程序在使用完后,应当调用free()将内存释放。总之,所有在native语言编程中应当注意的内存泄漏规则,在JNI编程中依然适应。

Native语言本身引入的内存泄漏会造成native memory的内存,严重情况下会造成native memoryout of memory

Global Reference引入的内存泄漏

JNI编程还要同时遵循JNI的规范标准,JVM附加了JNI编程特有的内存管理机制。

JNI中的Local Reference只在native method执行时存在,当native method执行完后自动失效。这种自动失效,使得对Local Reference的使用相对简单,native method执行完后,它们所引用的Java对象的reference count会相应减1。不会造成Java HeapJava对象的内存泄漏。

Global ReferenceJava对象的引用一直有效,因此它们引用的Java对象会一直存在Java Heap中。程序员在使用Global Reference时,需要仔细维护对Global Reference的使用。如果一定要使用Global Reference,务必确保在不用的时候删除。就像在C语言中,调用malloc()动态分配一块内存之后,调用free()释放一样。否则,Global Reference引用的Java对象将永远停留在Java Heap中,造成Java Heap的内存泄漏。


JNI编程中潜在的内存泄漏——对LocalReference的深入理解

Local Referencenative method执行完成后,会自动被释放,似乎不会造成任何的内存泄漏。但这是错误的。对Local Reference的理解不够,会造成潜在的内存泄漏。

本章重点阐述Local Reference使用不当可能引发的内存泄漏。引入两个错误实例,也是JNI程序员容易忽视的错误;在此基础上介绍Local Reference表,对比native method中的局部变量和JNI Local Reference的不同,使读者深入理解JNI Local Reference的实质;最后为JNI程序员提出应该如何正确合理使用JNI Local Reference,以避免内存泄漏。

错误实例 1

在某些情况下,我们可能需要在native method里面创建大量的JNI Local Reference。这样可能导致 native memory的内存泄漏,如果在native method返回之前native memory已经被用光,就会导致native memoryout of memory

在代码清单 1 里,我们循环执行count次,JNI functionNewStringUTF()在每次循环中从Java Heap中创建一个String对象,strJava Heap传给JNI native methodLocal Reference,每次循环中新创建的String对象覆盖上次循环中str的内容。str似乎一直在引用到一个String对象。整个运行过程中,我们看似只创建一个Local Reference

执行代码清单1的程序,第一部分为Java代码,nativeMethod(int i)中,输入参数设定循环的次数。第二部分为JNI代码,用C语言实现了nativeMethod(int i)

清单 1. Local Reference引发内存泄漏
 Java 代码部分
 class TestLocalReference { 
 private native void nativeMethod(int i); 
 public static void main(String args[]) { 
         TestLocalReference c = new TestLocalReference(); 
         //call the jni native method 
         c.nativeMethod(1000000); 
 }  
 static { 
 //load the jni library 
 System.loadLibrary("StaticMethodCall"); 
 } 
 } 

 JNI代码,nativeMethod(int i)的C语言实现
 #include<stdio.h> 
 #include<jni.h> 
 #include"TestLocalReference.h"
 JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod 
 (JNIEnv * env, jobject obj, jint count) 
 { 
 jint i = 0; 
 jstring str; 

 for(; i<count; i++) 
         str = (*env)->NewStringUTF(env, "0"); 
 } 
运行结果
 JVMCI161: FATAL ERROR in native method: Out of memory when expanding 
 local ref table beyond capacity 
 at TestLocalReference.nativeMethod(Native Method) 
 at TestLocalReference.main(TestLocalReference.java:9)

运行结果证明,JVM运行异常终止,原因是创建了过多的Local Reference,从而导致out of memory。实际上,nativeMethod在运行中创建了越来越多的JNI Local Reference,而不是看似的始终只有一个。过多的Local Reference,导致了JNI内部的JNI Local Reference表内存溢出。

错误实例 2

实例 2 是实例 1 的变种,Java代码未作修改,但是nativeMethod(int i)C语言实现稍作修改。在JNInative method中实现的utility函数中创建JavaString对象。utility函数只建立一个String对象,返回给调用函数,但是utility函数对调用者的使用情况是未知的,每个函数都可能调用它,并且同一函数可能调用它多次。在实例 2 中,nativeMethod在循环中调用count次,utility函数在创建一个String对象后即返回,并且会有一个退栈过程,似乎所创建的Local Reference会在退栈时被删除掉,所以应该不会有很多Local Reference被创建。实际运行结果并非如此。

清单 2. Local Reference引发内存泄漏
 Java 代码部分参考实例 1,未做任何修改。

 JNI 代码,nativeMethod(int i) 的 C 语言实现
 #include<stdio.h> 
 #include<jni.h> 
 #include"TestLocalReference.h"
 jstring CreateStringUTF(JNIEnv * env) 
 { 
 return (*env)->NewStringUTF(env, "0"); 
 } 
 JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod 
 (JNIEnv * env, jobject obj, jint count) 
 { 
 jint i = 0; 
 for(; i<count; i++) 
 { 
         str = CreateStringUTF(env); 
 } 
 } 
运行结果
 JVMCI161: FATAL ERROR in native method: Out of memory when expanding local ref 
 table beyond  capacity 
 at TestLocalReference.nativeMethod(Native Method) 
 at TestLocalReference.main(TestLocalReference.java:9)

运行结果证明,实例 2 的结果与实例 1 的完全相同。过多的Local Reference被创建,仍然导致了JNI内部的 JNI Local Reference表内存溢出。实际上,在utility函数CreateStringUTF(JNIEnv * env)

执行完成后的退栈过程中,创建的Local Reference并没有像native code中的局部变量那样被删除,而是继续在Local Reference表中存在,并且有效。Local Reference 和局部变量有着本质的区别。

Local Reference深层解析

Java JNI的文档规范只描述了JNI Local Reference是什么(存在的目的),以及应该怎么使用Local Reference(开放的接口规范)。但是对Java虚拟机中JNI Local Reference的实现并没有约束,不同的Java虚拟机有不同的实现机制。这样的好处是,不依赖于具体的JVM实现,有好的可移植性;并且开发简单,规定了“应该怎么做、怎么用”。但是弊端是初级开发者往往看不到本质,“不知道为什么这样做”。对Local Reference没有深层的理解,就会在编程过程中无意识的犯错。

Local ReferenceLocal Reference

理解Local Reference表的存在是理解JNI Local Reference的关键。

JNI Local Reference的生命期是在native method的执行期(从Java程序切换到native code环境时开始创建,或者在native method执行时调用JNI function创建),在 native method执行完毕切换回Java程序时,所有JNI Local Reference被删除,生命期结束(调用JNI function可以提前结束其生命期)。

实际上,每当线程从Java环境切换到native code上下文时(J2N),JVM会分配一块内存,创建一个Local Reference表,这个表用来存放本次native method执行中创建的所有的Local Reference。每当在native code中引用到一个Java对象时,JVM就会在这个表中创建一个Local Reference。比如,实例 1 中我们调用NewStringUTF()Java Heap中创建一个String对象后,在Local Reference表中就会相应新增一个Local Reference

图 1. Local Reference表、Local ReferenceJava对象的关系

image003

图 1 中:

⑴运行native method的线程的堆栈记录着Local Reference表的内存位置(指针 p)。

Local Reference表中存放JNI Local Reference,实现Local ReferenceJava对象的映射。

native method代码间接访问Java对象(java obj1,java obj2)。通过指针p定位相应的Local Reference的位置,然后通过相应的Local Reference映射到Java对象。

⑷ 当native method引用一个Java对象时,会在Local Reference表中创建一个新Local Reference。在Local Reference结构中写入内容,实现Local ReferenceJava对象的映射。

native method调用DeleteLocalRef()释放某个JNI Local Reference时,首先通过指针p定位相应的Local ReferenceLocal Ref表中的位置,然后从Local Ref表中删除该Local Reference,也就取消了对相应Java对象的引用(Ref count1)。

⑹当越来越多的Local Reference被创建,这些Local Reference会在Local Ref表中占据越来越多内存。当Local Reference太多以至于Local Ref表的空间被用光,JVM会抛出异常,从而导致JVM的崩溃。

Local Ref不是native code的局部变量

很多人会误将JNI中的Local Reference理解为Native Code的局部变量。这是错误的。

Native Code的局部变量和Local Reference是完全不同的,区别可以总结为:

⑴局部变量存储在线程堆栈中,而Local Reference存储在Local Ref表中。

⑵局部变量在函数退栈后被删除,而Local Reference在调用DeleteLocalRef()后才会从Local Ref表中删除,并且失效,或者在整个Native Method执行结束后被删除。

⑶ 可以在代码中直接访问局部变量,而Local Reference的内容无法在代码中直接访问,必须通过JNI function间接访问。JNI function实现了对Local Reference的间接访问,JNI function的内部实现依赖于具体JVM

代码清单 1 中 str = (*env)->NewStringUTF(env, "0");

strjstring类型的局部变量。Local Ref表中会新创建一个Local Reference,引用到NewStringUTF(env, "0")Java Heap中新建的String对象。如图 2 所示:

图 2. str 间接引用 string 对象image005

图 2 中,str 是局部变量,在 native method 堆栈中。Local Ref3 是新创建的 Local Reference,在 Local Ref 表中,引用新创建的 String 对象。JNI 通过 str 和指针 p 间接定位 Local Ref3,但 p 和 Local Ref3 对 JNI 程序员不可见。

Local Reference导致内存泄漏

在以上论述基础上,我们通过分析错误实例 1 和实例 2,来分析 Local Reference 可能导致的内存泄漏,加深对 Local Reference 的深层理解。

分析错误实例 1:

局部变量str在每次循环中都被重新赋值,间接指向最新创建的Local Reference,前面创建的Local Reference 一直保留在 Local Ref 表中。

在实例 1 执行完第i次循环后,内存布局如图 3:

图 3. 执行i次循环后的内存布局

image007

继续执行完第i+1次循环后,内存布局发生变化,如图 4:

图 4. 执行i+1次循环后的内存布局

image009

图 4 中,局部变量 str 被赋新值,间接指向了Local Ref i+1。在native method运行过程中,我们已经无法释放Local Ref i占用的内存,以及Local Ref i所引用的第istring对象所占据的Java Heap内存。所以,native memoryLocal Ref i被泄漏,Java Heap中创建的第 istring对象被泄漏了。

也就是说在循环中,前面创建的所有iLocal Reference都泄漏了native memory的内存,创建的所有 i 个string对象都泄漏了Java Heap的内存。

直到native memory执行完毕,返回到Java程序时(N2J),这些泄漏的内存才会被释放,但是 Local Reference 表所分配到的内存往往很小,在很多情况下N2J之前可能已经引发严重内存泄漏,导致Local Reference表的内存耗尽,使JVM崩溃,例如错误实例 1。

分析错误实例 2:

实例 2 与实例 1 相似,虽然每次循环中调用工具函数CreateStringUTF(env)来创建对象,但是在CreateStringUTF(env)返回退栈过程中,只是局部变量被删除,而每次调用创建的Local Reference仍然存在Local Ref表中,并且有效引用到每个新创建的string对象。str局部变量在每次循环中被赋新值。

这样的内存泄漏是潜在的,但是这样的错误 在JNI程序员编程过程中却经常出现。通常情况,在触发out of memory之前,native method已经执行完毕,切换回 Java 环境,所有 Local Reference 被删除,问题也就没有显露出来。但是某些情况下就会引发out of memory,导致实例 1 和实例 2 中的JVM崩溃。

控制Local Reference生命期

因此,在JNI编程时,正确控制JNILocal Reference的生命期。如果需要创建过多的Local Reference,那么在对被引用的Java对象操作结束后,需要调用JNI function(如 DeleteLocalRef()),及时将JNILocal ReferenceLocal Ref表中删除,以避免潜在的内存泄漏。

总结


本文阐述了JNI编程可能引发的内存泄漏,JNI编程既可能引发Java Heap的内存泄漏,也可能引发native memory的内存泄漏,严重的情况可能使JVM运行异常终止。JNI软件开发人员在编程中,应当考虑以下几点,避免内存泄漏:

  • native code本身的内存管理机制依然要遵循。
  • 使用Global reference时,当native code不再需要访问Global reference时,应当调用JNI函数DeleteGlobalRef()删除Global reference和它引用的Java对象。Global reference管理不当会导致Java Heap的内存泄漏。
  • 透彻理解Local reference,区分Local referencenative code的局部变量,避免混淆两者所引起的native memory的内存泄漏。
  • 使用Local reference时,如果Local reference引用了大的Java对象,当不再需要访问Local reference时,应当调用JNI函数DeleteLocalRef()删除Local reference,从而也断开对Java对象的引用。这样可以避免Java Heapout of memory
  • 使用Local reference时,如果在native method执行期间会创建大量的Local reference,当不再需要访问Local reference时,应当调用JNI函数DeleteLocalRef()删除Local referenceLocal reference表空间有限,这样可以避免Local reference表的内存溢出,避免native memoryout of memory
  • 严格遵循JavaJNI规范书中的使用规则。

参考链接


在 JNI 编程中避免内存泄漏

Java泛型:类型擦除

类型擦除


Class c1 = new ArrayList<Integer>().getClass();
Class c2 = new ArrayList<String>().getClass(); 
System.out.println(c1 == c2);

/* Output
true
*/

显然在平时使用中,ArrayList<Integer>()new ArrayList<String>()是完全不同的类型,但是在这里,程序却的的确确会输出true

这就是Java泛型的类型擦除造成的,因为不管是ArrayList<Integer>()还是new ArrayList<String>(),都在编译器被编译器擦除成了ArrayList。 那编译器为什么要做这件事?原因也和大多数的Java让人不爽的点一样——兼容性。由于泛型并不是从Java诞生就存在的一个特性,而是等到SE5才被加 入的,所以为了兼容之前并未使用泛型的类库和代码,不得不让编译器擦除掉代码中有关于泛型类型信息的部分,这样最后生成出来的代码其实是『泛型无关』的, 我们使用别人的代码或者类库时也就不需要关心对方代码是否已经『泛化』,反之亦然。

在编译器层面做的这件事(擦除具体的类型信息),使得Java的泛型先天都存在一个让人非常难受的缺点:

在泛型代码内部,无法获得任何有关泛型参数类型的信息。

List<Integer> list = new ArrayList<Integer>();
Map<Integer, String> map = new HashMap<Integer, String>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));

/* Output
[E]
[K, V]
*/

关于getTypeParameters()的解释:

Returns an array of TypeVariable objects that represent the type variables declared by the generic declaration represented by this GenericDeclaration object, in declaration order. Returns an array of length 0 if the underlying generic declaration declares no type variables.

我们期待的是得到泛型参数的类型,但是实际上我们只得到了一堆占位符。

public class Main<T> {

    public T[] makeArray() {
        // error: Type parameter 'T' cannot be instantiated directly
        return new T[5];
    }
}

我们无法在泛型内部创建一个T类型的数组,原因也和之前一样,T仅仅是个占位符,并没有真实的类型信息,实际上,除了new表达式之外,instanceof操作和转型(会收到警告)在泛型内部都是无法使用的,而造成这个的原因就是之前讲过的编译器对类型信息进行了擦除。

同时,面对泛型内部形如T var;的代码时,记得多念几遍:它只是个Object,它只是个Object……

public class Main<T> {

    private T t;

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public static void main(String[] args) {
        Main<String> m = new Main<String>();
        m.set("findingsea");
        String s = m.get();
        System.out.println(s);
    }
}

/* Output
findingsea
*/

虽然有类型擦除的存在,使得编译器在泛型内部其实完全无法知道有关T的任何信息,但是编译器可以保证重要的一点:内部一致性,也是我们放进去的是什么类型的对象,取出来还是相同类型的对象,这一点让Java的泛型起码还是有用武之地的。

代码片段四展现就是编译器确保了我们放在t上的类型的确是T(即便它并不知道有关T的任何类型信息)。这种确保其实做了两步工作:

  • set()处的类型检验
  • get()处的类型转换

这两步工作也成为边界动作

public class Main<T> {

    public List<T> fillList(T t, int size) {
        List<T> list = new ArrayList<T>();
        for (int i = 0; i < size; i++) {
            list.add(t);
        }
        return list;
    }

    public static void main(String[] args) {
        Main<String> m = new Main<String>();
        List<String> list = m.fillList("findingsea", 5);
        System.out.println(list.toString());
    }
}

/* Output
[findingsea, findingsea, findingsea, findingsea, findingsea]
*/

代码片段五同样展示的是泛型的内部一致性。

擦除的补偿


如上看到的,但凡是涉及到确切类型信息的操作,在泛型内部都是无法共工作的。那是否有办法绕过这个问题来编程,答案就是显示地传递类型标签。

public class Main<T> { 
    public T create(Class<T> type) { 
        try { 
            return type.newInstance(); 
        } catch (Exception e) { 
            e.printStackTrace(); 
        } 
        return null; 
    } 
    public static void main(String[] args) { 
        Main<String> m = new Main<String>(); 
        String s = m.create(String.class); 
    } 
}

代码片段六展示了一种用类型标签生成新对象的方法,但是这个办法很脆弱,因为这种办法要求对应的类型必须有默认构造函数,遇到Integer类型的时候就失败了,而且这个错误还不能在编译器捕获。

进阶的方法可以用限制类型的显示工厂和模板方法设计模式来改进这个问题,具体可以参见《Java编程思想 (第4版)》P382。

public class Main<T> { 
    public T[] create(Class<T> type) { 
        return (T[]) Array.newInstance(type, 10); 
    } 
    public static void main(String[] args) {
        Main<String> m = new Main<String>(); 
        String[] strings = m.create(String.class); 
    } 
}

代码片段七展示了对泛型数组的擦除补偿,本质方法还是通过显示地传递类型标签,通过Array.newInstance(type, size)来生成数组,同时也是最为推荐的在泛型内部生成数组的方法。

参考链接


Java泛型:类型擦除
java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题

JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8: illegal start byte 0xfe

在使用Jni的JNIEnv->NewStringUTF的时候抛出了异常"JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8: illegal start byte 0xfe "。网上搜索了一下,这个异常是由于Java虚拟机内部的dalvik/vm/CheckJni.c中的checkUtfString函数抛出的,并且JVM的这个接口明确是不支持四个字节的UTF8字符。因此需要在调用函数之前,对接口传入的字符串进行过滤,过滤函数如下:

int checkUtfString(const char* bytes)
{
    const char* origBytes = bytes;
    if (bytes == NULL) {
        return -1;
    }
    while (*bytes != '\0') {
       unsigned char utf8 = *(bytes++);
        // Switch on the high four bits.
        switch (utf8 >> 4) {
            case 0x00:
            case 0x01:
            case 0x02:
            case 0x03:
            case 0x04:
            case 0x05:
            case 0x06:
            case 0x07: {
                // Bit pattern 0xxx. No need for any extra bytes.
                break;
            }
            case 0x08:
            case 0x09:
            case 0x0a:
            case 0x0b:
            case 0x0f: {
                /*printf("****JNI WARNING: illegal start byte 0x%x\n", utf8);*/
                return -1;
            }
            case 0x0e: {
                // Bit pattern 1110, so there are two additional bytes.
                utf8 = *(bytes++);
                if ((utf8 & 0xc0) != 0x80) {
                    /*printf("****JNI WARNING: illegal continuation byte 0x%x\n", utf8);*/
                    return -1;
                }
                // Fall through to take care of the final byte.
            }
            case 0x0c:
            case 0x0d: {
                // Bit pattern 110x, so there is one additional byte.
                utf8 = *(bytes++);
                if ((utf8 & 0xc0) != 0x80) {
                    /*printf("****JNI WARNING: illegal continuation byte 0x%x\n", utf8);*/
                    return -1;
                }
                break;
            }
        }
    }
    return 0;
}

参考链接 android jni中 utf-8的check

使用 Mockito 单元测试

目录


1. 需求知识

2. 使用 存根(Stub) 和 模拟对象(Mock Object) 进行测试

2.1. 为什么需要模拟?

2.2. 存根(Stub) vs. 模拟对象 (Mock)

2.3. 行为测试 vs. 状态测试

2.4. 生成模拟对象

3. 模拟框架( Mock Framework)

4. Mockito

4.1. 使用 Mockito 模拟对象

4.2. 使用 Mockito

4.3. Mockito的限制

4.4. 模拟对象的配置

4.5. 验证模拟对象的行为

4.6. Spy

5. Mockito 在 Android 平台测试

5.1. 在 Android 使用 Mockito

5.2. 安装

6. 链接和参考

1.需求知识

该教程需要理解单元测试和熟悉JUnit框架的使用。

如果您不熟悉JUnit,请阅读JUnit教程。

2. 使用 存根(Stub) 和 模拟对象(Mock Object) 进行测试

2.1. 为什么需要模拟?

一个单元测试需要在隔离的环境下执行。如果可以的话需要消除其他依赖的服务影响。但实际上,软件中是充满依赖关系的.我们会基于service类写操作类,而service类又是基于数据访问类(DAOs)的,依次下去.

为了解决这个问题, 可以使用 存根 (Stub) 或者 模拟 (Mock) 对象的方法进行测试。

2.2. 存根(Stub) vs. 模拟对象 (Mock)

存根(Stub)类是实现了一个接口或者抽象类的类,可以在测试过程中使用该类,例如:

public class TestStub {
	static interface USB {
		void work();
	}

	static class Mp3Stub implements USB {
		@Override
		public void work() {
			// code.

		}
	 }

	static class Mp4Stub implements USB {
		@Override
		public void work() {
			// code.
		}
	}
}

一个模拟对象(mock object)是一个接口或者抽象类的虚拟实现。例如:

public class TestMock {
	static interface USB {
		void work();
	}

	@Test
	public void testMockObject() {
		USB usb = Mockito.mock( USB.class );
		usb.work();
	}
}

存根和模拟对象都可以传递给其他的对象进行测试。你的一些单元测试可以测这些类的正确性等。利用存根对象或者模拟对象可以保证测试过程中不受到其他的影响。

存根对象需要自定义实现方法;

模拟对象只需要更少的代码和简单的配置。

以下的内容将详细介绍模拟对象的使用方法。

2.3. 行为测试 vs. 状态测试

Mock 对象允许你对行为进行测试。有一些测试不需要验证结果,但是需要检查某些方法是否被正确的参数调用过。这种测试为行为测试。

状态测试只是关注与结果是否正确,而行为测试能够判断一个应用调用结构以及层次。

2.4. 生成模拟对象

你们可以使用Mock 框架来生成模拟对象。Mock 框架允许你在运行期间创建对象,并且定义它的一些行为。

一个典型的例子就是使用模拟对象来模拟数据库DAO层。在生产环境上是使用运行的数据库,但是在单元测试环境中完全可以用模拟对象来模拟数据,确保单元测试的正确条件。这样就不需要依赖于外部的数据。

3. 模拟框架( Mock Framework)

比较流行的模拟框架有 EasyMock、jMock 和 Mockito。下面的列表是这些框架的链接。
# jMock
http://jmock.org/
# EasyMock
http://easymock.org/
# Mockito
http://mockito.org/

4. Mockito

4.1. 使用 Mockito 模拟对象

Mockito 是比较流行的模拟框架,可以与JUnit 联合起来测试。它允许你进行创建和配置模拟对象。

Mockito的官方网站: Mockito 主页.

4.2. 使用 Mockito

Mockito 支持使用 mock() 静态方法创建模拟对象。

同样也支持 @Mock注解方式,如果使用注解的方式,需要使用在初始化方法调用 MockitoAnnotation.InitMock( this ) 方法

例如,下面的例子就是使用 Mockito 进行对类 ClassToTest 的单元测试。

public class MockitoTest {
	@Mock
	MyDatabase databaseMock;

	@Before
	public void setUp() throws Exception {
		MockitoAnnotations.initMocks(this);
	}


	@Test
	public void testQuery() {
		// 需要测试的类
		ClassToTest t = new ClassToTest(databaseMock);
		// 调用方法
		boolean check = t.query("* from t");
		// 验证结果
		assertTrue(check);
		// 模拟对象是否调用了该方法
		Mockito.verify( databaseMock ).query("* from t");
	}
}

提示

可以使用静态导入方法调用方法 mock()

4.3. Mockito的限制

Mockito 以下的类型不能进行构造:

  • 终态类(final classes)
  • 匿名类(anonymous classes)
  • 基本数据类型(primitive types)

4.4. 模拟对象的配置

Mockito 可以使用 verify() 方法来确认某些方法是否被调用过.

when(....).thenReturn(....) 结构可以为某些条件给定一个预期的返回值.

@Test
public void testList() {
	List mock = Mockito.mock( List.class );
	Mockito.when( mock.get( 0 ) ).thenReturn( 1 );
	assertEquals( "预期返回1", 1, mock.get( 0 ) );
}

同样可以使用doReturn(object).when(kdskfsk).methodCall 结构

4.5. 验证模拟对象的行为

Mockito 跟踪了所有的方法调用和参数的调用情况。verify()可以验证方法的行为。

查看下面的例子:

@Test
public void testMap() {
	Map mock = Mockito.mock( Map.class );
	Mockito.when( mock.get( "city" ) ).thenReturn( "深圳" );
	// test code
	assertEquals( "城市测试", "深圳", mock.get( "city" ) );
	Mockito.verify(mock).get( Matchers.eq( "city" ) );
	Mockito.verify( mock, Mockito.times( 2 ) );
}

4.6. Spy

@Spy 或者方法 spy() 可以包含一个真实的对象. 每次调用,除非特出指定,委托给改真实对象的调用.

@Test
public void testSpy() {
	// Lets mock a LinkedList
	List list = new LinkedList();
	list.add( "yes" );
	List spy = Mockito.spy(list);
	//You have to use doReturn() for stubbing
	assertEquals( "yes", spy.get( 0 ) );
	Mockito.doReturn("foo").when(spy).get(0);
	assertEquals( "foo", spy.get( 0 ) );
 }


@Test( expected = IndexOutOfBoundsException.class)
public void testSpy2() {
	// Lets mock a LinkedList
	List list = new LinkedList();
	List spy = Mockito.spy(list);
	// this would not work
	// real method is called so spy.get(0)
	// throws IndexOutOfBoundsException (list is still empty)
	Mockito.when(spy.get(0)).thenReturn("foo");
	assertEquals( "foo", spy.get( 0 ) );
}

5. Mockito 在 Android 平台测试

5.1. 在 Android 使用 Mockito

Mockito 同样也可以在安卓平台上进行测试。

5.2. 安装

在 Android 测试项目中使用 Mockito。添加下面的包到Android 测试项目的 libs 目录
https://mockito.googlecode.com/files/mockito-all-1.9.5.jar
http://dexmaker.googlecode.com/files/dexmaker-1.0.jar
http://dexmaker.googlecode.com/files/dexmaker-mockito-1.0.jar
接下来可以在你的测试项目中使用 Mockito 。

6. 链接和参考

Mockito 项目主页
Mockito 的依赖注入功能
Unit tests with Mockito - Tutorial
使用 Mockito 单元测试 – 教程