让tomcat使用强制ETag参数解除浏览器对静态文件的缓存

Etag在HTTP1.1中有介绍,主要的作用就是在(css file, image, javascript file)文件

请求返回的http头加入ETag参数,Etag有服务器端生成,并且随着文件的改变而改变,这样浏览器端就会只重新请求获取 Etag发生变化的文件,减少浏览器端数据的流量,加快浏览器的反应速度,重要的是减轻服务器端的压力,所以服务器端Etag的实现就比较重要了。

ETag有两种,一种是弱类型的(Weak ETag),一种是强类型的(Strong ETag),强类型格式为ETag=”文件长-最后修改时间”,弱类型是在前面加上W/如:W/”文件长-最后修改时间”,在浏览器中监控如下:

1.第一次请求一个静态文件返回:

HTTP/1.1 200 Ok
ETag: W/"1837-1431071955000"

2.第二次请求:

If-None-Match: W/"1837-1431071955000" 
If-Modified-Since: Fri, 08 May 2015 07:59:15 GMT

返回:

HTTP/1.1 304 Not Modified 
ETag: W/"1837-1431071955000" 
Date: Fri, 28 Oct 2016 02:47:04 GMT 

默认情况下使用弱类型的ETag浏览器会忽略设个机制,而且tomcat默认就是使用的这种机制,所以当你更新一个静态文件比如脚本啊或者一个样式文件,发现浏览器里面还是原来的文件,必须清理缓存或者在文件后面加上一个参数才能自动更新浏览器的缓存,而且tomcat也没有可配置的地方能够配置tomcat使用请类型的ETag,下面提供一个方法来修改这个机制,就是继承类org.apache.naming.resources.FileDirContext,重写doGetAttributes,在返回的属性中加入强类型ETag就可以了,代码如下:

package cn.fullstacks.tomcat;

import java.io.File;

import javax.naming.NamingException;
import javax.naming.directory.Attributes;

/**
 * 在http返回头中返回强类型ETag代替tomcat的弱类型ETag
 * 在context.xml中配置
 * <Resources className="cn.fullstacks.tomcat.ETagFileDirContext" />
 * @author Administrator
 *
 */
public class ETagFileDirContext extends org.apache.naming.resources.FileDirContext {

	@Override
	protected Attributes doGetAttributes(String name, String[] attrIds)
			throws NamingException {
		File file = file(name);

		if (file == null) {
			return null;
		}
		FileResourceAttributes fra = new FileResourceAttributes(file);
		fra.setETag("\"" + fra.getContentLength() + "-" +fra.getLastModified() + "\"");
		return fra;
	}

}

在tomcat的配置文件context.xml中加入配置如下:

<context>
    <Resources className="cn.fullstacks.tomcat.ETagFileDirContext" />
...
</context>

这样请求返回的ETag如下:

HTTP/1.1 200 Ok
ETag: "7664-1477637145682"

这样每次更新静态文件只要最后修改日期变更,那么浏览器必定会重新加载,这样就解决了浏览器静态文件缓存更新的问题。



OAuth的改变(转载)

作者:郭无心
链接:http://www.zhihu.com/question/19851243/answer/75070070
来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

OAuth1.0

OAuth诞生前,Web安全方面的标准协议只有OpenID,不过它关注的是验证,即WHO的问题,而不是授权,即WHAT的问题。好在FlickrAuth和GoogleAuthSub等私有协议在授权方面做了不少有益的尝试,从而为OAuth的诞生奠定了基础。

OAuth1.0定义了三种角色:User、Service Provider、Consumer。如何理解?假设我们做了一个SNS,它有一个功能,可以让会员把他们在Google上的联系人导入到SNS上,那么此时的会员是User,Google是Service Providere,而SNS则是Consumer。

+----------+                                           +----------+
 |          |--(A)- Obtaining a Request Token --------->|          |
 |          |                                           |          |
 |          |<-(B)- Request Token ----------------------|          |
 |          |       (Unauthorized)                      |          |
 |          |                                           |          |
 |          |      +--------+                           |          |
 |          |>-(C)-|       -+-(C)- Directing ---------->|          |
 |          |      |       -+-(D)- User authenticates ->|          |
 |          |      |        |      +----------+         | Service  |
 | Consumer |      | User-  |      |          |         | Provider |
 |          |      | Agent -+-(D)->|   User   |         |          |
 |          |      |        |      |          |         |          |
 |          |      |        |      +----------+         |          |
 |          |<-(E)-|       -+-(E)- Request Token ------<|          |
 |          |      +--------+      (Authorized)         |          |
 |          |                                           |          |
 |          |--(F)- Obtaining a Access Token ---------->|          |
 |          |                                           |          |
 |          |<-(G)- Access Token -----------------------|          |
 +----------+                                           +----------+

花絮:OAuth1.0的RFC没有ASCII流程图,于是我敲了几百下键盘自己画了一个,后经网友提示,Emacs可以很轻松的搞定ASCII图:Emacs Screencast: Artist Mode,VIM当然也可以搞定,不过要借助一个插件:DrawIt,可惜我的键盘都要坏了。

Consumer申请Request Token(/oauth/1.0/request_token):

oauth_consumer_key
oauth_signature_method
oauth_signature
oauth_timestamp
oauth_nonceoauth_version

Service Provider返回Request Token:

oauth_token
oauth_token_secret

Consumer重定向User到Service Provider(/oauth/1.0/authorize):

oauth_token
oauth_callback

Service Provider在用户授权后重定向User到Consumer:

oauth_token

Consumer申请Access Token(/oauth/1.0/access_token):

oauth_consumer_key
oauth_token
oauth_signature_method
oauth_signature
oauth_timestamp
oauth_nonceoauth_version

Service Provider返回Access Token:

oauth_token
oauth_token_secret

注:整个操作流程中,需要注意涉及两种Token,分别是Request Token和Access Token,其中Request Token又涉及两种状态,分别是未授权和已授权。

OAuth1.0a

OAuth1.0存在安全漏洞,详细介绍:Explaining the OAuth Session Fixation Attack,还有这篇:How the OAuth Security Battle Was Won, Open Web Style

简单点来说,这是一种会话固化攻击,和常见的会话劫持攻击不同的是,在会话固化攻击中,攻击者会初始化一个合法的会话,然后诱使用户在这个会话上完成后续操作,从而达到攻击的目的。反映到OAuth1.0上,攻击者会先申请Request Token,然后诱使用户授权这个Request Token,接着针对回调地址的使用,又存在以下几种攻击手段:

  • 如果Service Provider没有限制回调地址(应用设置没有限定根域名一致),那么攻击者可以把oauth_callback设置成成自己的URL,当User完成授权后,通过这个URL自然就能拿到User的Access Token。
  • 如果Consumer不使用回调地址(桌面或手机程序),而是通过User手动拷贝粘贴Request Token完成授权的话,那么就存在一个竞争关系,只要攻击者在User授权后,抢在User前面发起请求,就能拿到User的Access Token。

为了修复安全问题,OAuth1.0a出现了(RFC5849),主要修改了以下细节:

  • Consumer申请Request Token时,必须传递oauth_callback,而Consumer申请Access Token时,不需要传递oauth_callback。通过前置oauth_callback的传递时机,让oauth_callback参与签名,从而避免攻击者假冒oauth_callback。
  • Service Provider获得User授权后重定向User到Consumer时,返回oauth_verifier,它会被用在Consumer申请Access Token的过程中。攻击者无法猜测它的值。

Consumer申请Request Token(/oauth/1.0a/request_token):

oauth_consumer_key
oauth_signature_method
oauth_signature
oauth_timestamp
oauth_nonceoauth_version
oauth_callback

Service Provider返回Request Token:

oauth_token
oauth_token_secret
oauth_callback_confirmed

Consumer重定向User到Service Provider(/oauth/1.0a/authorize):

oauth_token

Service Provider在用户授权后重定向User到Consumer:

oauth_token
oauth_verifier

Consumer申请Access Token(/oauth/1.0a/access_token):

oauth_consumer_key
oauth_token
oauth_signature_method
oauth_signature
oauth_timestamp
oauth_nonceoauth_version
oauth_verifier

Service Provider返回Access Token:

oauth_token
oauth_token_secret

注:Service Provider返回Request Token时,附带返回的oauth_callback_confirmed是为了说明Service Provider是否支持OAuth1.0a版本。

签名参数中,oauth_timestamp表示客户端发起请求的时间,如未验证会带来安全问题。

在探讨oauth_timestamp之前,先聊聊oauth_nonce,它是用来防止重放攻击的,Service Provider应该验证唯一性,不过保存所有的oauth_nonce并不现实,所以一般只保存一段时间(比如最近一小时)内的数据。

如果不验证oauth_timestamp,那么一旦攻击者拦截到某个请求后,只要等到限定时间到了,oauth_nonce再次生效后就可以把请求原样重发,签名自然也能通过,完全是一个合法请求,所以说Service Provider必须验证oauth_timestamp和系统时钟的偏差是否在可接受范围内(比如十分钟),如此才能彻底杜绝重放攻击。

需要单独说一下桌面或手机应用应该如何使用OAuth1.0a。此类应用通常没有服务端,无法设置Web形式的oauth_callback地址,此时应该把它设置成oob(out-of-band),当用户选择授权后,Service Provider在页面上显示PIN码(也就是oauth_verifier),并引导用户把它粘贴到应用里完成授权。

一个问题是应用如何打开用户授权页面呢?很容易想到的做法是使用内嵌浏览器,说它是个错误的做法或许有点偏激,但它至少是个对用户不友好的做法,因为一旦浏览器内嵌到程序里,那么用户输入的用户名密码就有被监听的可能;对用户友好的做法应该是打开新窗口,弹出系统默认的浏览器,让用户在可信赖的上下文环境中完成授权流程。

不过这样的方式需要用户在浏览器和应用间手动切换,才能完成授权流程,某种程度上说,影响了用户体验,好在可以通过一些其它的技巧来规避这个问题,其中一个行之有效的办法是Monitor web-browser title-bar,简单点说,操作系统一般提供相应的API可以让应用监听桌面上所有窗口的标题,应用一旦发现某个窗口标题符合预定义格式,就可以认为它是我们要的PIN码,无需用户参与就可以完成授权流程。Google支持这种方式,并且有资料专门描述了细节:Auto-Detecting Approval(注:墙!)。

还有一点需要注意的是对桌面或移动应用来说,consumer_key和consumer_secret通常都是直接保存在应用里的,所以对攻击者而言,理论上可以通过反编译之类的手段解出来。进而通过consumer_key和consumer_secret签名一个伪造的请求,并且在请求中把oauth_callback设置成自己控制的URL,来骗取用户授权。为了屏蔽此类问题,Service Provider需要强制开发者必须预定义回调地址:如果预定义的回调地址是URL方式的,则需要验证请求中的回调地址和预定义的回调地址是否主域名一致;如果预定义的回调地址是oob方式的,则禁止请求以URL的方式回调。

OAuth2.0

OAuth1.0虽然在安全性上经过修补已经没有问题了,但还存在其它的缺点,其中最主要的莫过于以下两点:其一,签名逻辑过于复杂,对开发者不够友好;其二,授权流程太过单一,除了Web应用以外,对桌面、移动应用来说不够友好。

为了弥补这些短板,OAuth2.0做了以下改变:

首先,去掉签名,改用SSL(HTTPS)确保安全性,所有的token不再有对应的secret存在,这也直接导致OAuth2.0不兼容老版本。

其次,针对不同的情况使用不同的授权流程,和老版本只有一种授权流程相比,新版本提供了四种授权流程,可依据客观情况选择。

在详细说明授权流程之前,我们需要先了解一下OAuth2.0中的角色:

OAuth1.0定义了三种角色:User、Service Provider、Consumer。而OAuth2.0则定义了四种角色:Resource Owner、Resource Server、Client、Authorization Server:

  • Resource Owner:User
  • Resource Server:Service Provider
  • Client:Consumer
  • Authorization Server:Service Provider

也就是说,OAuth2.0把原本OAuth1.0里的Service Provider角色分拆成Resource Server和Authorization Server两个角色,在授权时交互的是Authorization Server,在请求资源时交互的是Resource Server,当然,有时候他们是合二为一的。

下面我们具体介绍一下OAuth2.0提供的四种授权流程:

Authorization Code

可用范围:此类型可用于有服务端的应用,是最贴近老版本的方式。

+----------+
 | resource |
 |   owner  |
 |          |
 +----------+
      ^
      |
     (B)
 +----|-----+          Client Identifier      +---------------+
 |         -+----(A)-- & Redirection URI ---->|               |
 |  User-   |                                 | Authorization |
 |  Agent  -+----(B)-- User authenticates --->|     Server    |
 |          |                                 |               |
 |         -+----(C)-- Authorization Code ---<|               |
 +-|----|---+                                 +---------------+
   |    |                                         ^      v
  (A)  (C)                                        |      |
   |    |                                         |      |
   ^    v                                         |      |
 +---------+                                      |      |
 |         |>---(D)-- Authorization Code ---------'      |
 |  Client |          & Redirection URI                  |
 |         |                                             |
 |         |<---(E)----- Access Token -------------------'
 +---------+       (w/ Optional Refresh Token)

Client向Authorization Server发出申请(/oauth/2.0/authorize):

response_type = code
client_id
redirect_uri
scope
state

Authorization Server在Resource Owner授权后给Client返回Authorization Code:

code
state

Client向Authorization Server发出申请(/oauth/2.0/token):

grant_type = authorization_code
code
client_id
client_secret
redirect_uri

Authorization Server在Resource Owner授权后给Client返回Access Token:

access_token
token_type
expires_in
refresh_token

说明:基本流程就是拿Authorization Code换Access Token。

Implicit Grant

可用范围:此类型可用于没有服务端的应用,比如Javascript应用。

+----------+
 | Resource |
 |  Owner   |
 |          |
 +----------+
      ^
      |
     (B)
 +----|-----+          Client Identifier     +---------------+
 |         -+----(A)-- & Redirection URI --->|               |
 |  User-   |                                | Authorization |
 |  Agent  -|----(B)-- User authenticates -->|     Server    |
 |          |                                |               |
 |          |<---(C)--- Redirection URI ----<|               |
 |          |          with Access Token     +---------------+
 |          |            in Fragment
 |          |                                +---------------+
 |          |----(D)--- Redirection URI ---->|   Web-Hosted  |
 |          |          without Fragment      |     Client    |
 |          |                                |    Resource   |
 |     (F)  |<---(E)------- Script ---------<|               |
 |          |                                +---------------+
 +-|--------+
   |    |
  (A)  (G) Access Token
   |    |
   ^    v
 +---------+
 |         |
 |  Client |
 |         |
 +---------+

Client向Authorization Server发出申请(/oauth/2.0/authorize):

response_type = token
client_id
redirect_uri
scope
state

Authorization Server在Resource Owner授权后给Client返回Access Token:

access_token
token_type
expires_in
scope
state

说明:没有服务端的应用,其信息只能保存在客户端,如果使用Authorization Code授权方式的话,无法保证client_secret的安全。BTW:不返回Refresh Token。

Resource Owner Password Credentials

可用范围:不管有无服务端,此类型都可用。

+----------+
 | Resource |
 |  Owner   |
 |          |
 +----------+
      v
      |    Resource Owner
     (A) Password Credentials
      |
      v
 +---------+                                  +---------------+
 |         |>--(B)---- Resource Owner ------->|               |
 |         |         Password Credentials     | Authorization |
 | Client  |                                  |     Server    |
 |         |<--(C)---- Access Token ---------<|               |
 |         |    (w/ Optional Refresh Token)   |               |
 +---------+                                  +---------------+

Clien向Authorization Server发出申请(/oauth/2.0/token):

grant_type = password
username
password
scope

AuthorizationServer给Client返回AccessToken:

access_token
token_type
expires_in
refresh_token

说明:因为涉及用户名和密码,所以此授权类型仅适用于可信赖的应用。

Client Credentials

可用范围:不管有无服务端,此类型都可用。

+---------+                                  +---------------+
 |         |                                  |               |
 |         |>--(A)- Client Authentication --->| Authorization |
 | Client  |                                  |     Server    |
 |         |<--(B)---- Access Token ---------<|               |
 |         |                                  |               |
 +---------+                                  +---------------+

Client向Authorization Server发出申请(/oauth/2.0/token):

grant_type = client_credentials
client_id
client_secret
scope

Authorization Server给Client返回Access Token:

access_token
token_type
expires_in

说明:此授权类型仅适用于获取与用户无关的公共信息。BTW:不返回Refresh Token。

流程中涉及两种Token,分别是Access Token和Refresh Token。通常,Access Token的有效期比较短,而Refresh Token的有效期比较长,如此一来,当Access Token失效的时候,就需要用Refresh Token刷新出有效的Access Token:

+--------+                                         +---------------+
 |        |--(A)------- Authorization Grant ------->|               |
 |        |                                         |               |
 |        |<-(B)----------- Access Token -----------|               |
 |        |               & Refresh Token           |               |
 |        |                                         |               |
 |        |                            +----------+ |               |
 |        |--(C)---- Access Token ---->|          | |               |
 |        |                            |          | |               |
 |        |<-(D)- Protected Resource --| Resource | | Authorization |
 | Client |                            |  Server  | |     Server    |
 |        |--(E)---- Access Token ---->|          | |               |
 |        |                            |          | |               |
 |        |<-(F)- Invalid Token Error -|          | |               |
 |        |                            +----------+ |               |
 |        |                                         |               |
 |        |--(G)----------- Refresh Token --------->|               |
 |        |                                         |               |
 |        |<-(H)----------- Access Token -----------|               |
 +--------+           & Optional Refresh Token      +---------------+

Client向Authorization Server发出申请(/oauth/2.0/token):

grant_type = refresh_token
refresh_token
client_id
client_secret
scope

Authorization Server给Client返回Access Token:

access_token
expires_in
refresh_token
scope

JAMES 垃圾邮件过滤

最近公司发布了一个JAMES邮件服务器, 第二天早一来, 发现spool里面有几万的垃圾邮件.
在网上仔细查了一下, 一般都是自定义一个matcher和一个mailet, 如:

package com.easynet.mailet;

import java.util.Collection;
import java.util.Iterator;
import java.util.Vector;
import javax.mail.MessagingException;
import org.apache.mailet.GenericMatcher;
import org.apache.mailet.Mail;
import org.apache.mailet.MailAddress;

public class FilterSenderMatcher extends GenericMatcher {
	private static  String[] hosts = null;
	@Override
	public Collection match(Mail mail) throws MessagingException {
		//System.out.println("getCondition:" + getCondition());
		if (hosts == null && getCondition() != null) {
			hosts = getCondition().split(",");
		}
		if (hosts == null || hosts.length == 0 || getCondition() == null) {
			return mail.getRecipients();
		}
		MailAddress mailAddress = mail.getSender();
		//System.out.println("mailAddress.toString():" + mailAddress.toString());
		for (String strTemp : hosts) {
			if (mailAddress.toString().toLowerCase().indexOf(strTemp) != -1) {
				//System.out.println(mailAddress.toString().toLowerCase().indexOf(strTemp));
				return mail.getRecipients();
			}
		}
		//mail.getRecipients().clear();//code1
        return null;
	}
}

可发现这种做法根本就过滤不了后缀是本服务器域名的邮件, 但JAMES包下的:org\apache\james\transport\matchers很多matche是这种写法. 经过尝试, 得把code1处的注释打开才可以过滤.
正确的操作步骤:
1.编写自己的Matcher, 上面一段code(把code1处的注释打开), 和Mailet.

package com.easynet.mailet;
import javax.mail.MessagingException;
import org.apache.mailet.GenericMailet;
import org.apache.mailet.Mail;
import org.apache.mailet.MailAddress;
public class FilterSenderMailet extends GenericMailet {
	@Override
	public void service(Mail mail) throws MessagingException {
//		MailAddress ma = mail.getSender();   
//        System.out.println("sender:"+ma.toInternetAddress().toString()); 
	}
}

2. 把这两个JAVA类打包成JAR包, 放到james-2.3.2\apps\james\SAR-INF\lib目录下, 同时也要把james-2.3.2\apps\james.sar里面的james-2.3.2.jar,mail-1.4.1.jar,mailet-2.3.jar,mailet-api-2.3.jar这些包解压出来也放到这个目录下面, 不然会报找不到类的异常.
3.修改config.xml文件, 在<mailetpackages>后面添加:<mailetpackage>com.easynet.mailet</mailetpackage>, 在<matcherpackages>后面添加:<matcherpackage>com.easynet.mailet</matcherpackage>, 即是刚才的那两个类的包名.
4.修改config.xml文件, 添加过滤,在<spoolmanager>下面,<mailet match=”All” class=”PostmasterAlias”/>的后面添加:<mailet match=”FilterSenderMatcher=@easynet.com,@easynet.cn”  class=”FilterSenderMailet”/>, FilterSenderMatcher即为Mathcer的类的名字, FilterSenderMailet也mailet的类的名字, 它会在上面配置的包下面去找这两个类, 先通过matcher, 如果matcher返回的不是null的collection, 刚会下一步调用mailet; 如果matcher返回null, 但mail.getRecipients()不为null, 邮件会发送成功, 但不调用mailet, 所以code1处的注释要打开才不会发送.
OK,大功告成!

nginx和tomcat集成后重定向引发的问题解决

nginx作为反向代理,监听端口非80端口比如使用88端口,tomcat监听的端口8080,这种情况下当发生302重定向的时候,tomcat默认会重定向到80端口,根本原因就是tomcat的repose的头部带的location的端口默认是80端口,这样nginx就会重定向到80端口导致系统无法访问。如果nginx监听的是80端口自然不会存在这样的问题。

知道问题的根本原因是头部的location不对导致的,那么处理办法就很简单了,这里有两种办法:

1.治标不治本的办法,配置nginx,修改location达到解决问题

proxy_redirect     http://host http://host:88;

2.从根本上解决问题,修改tomcat配置,配置代理的端口

在server.xml配置文件中http的connector节点加入了proxyPort="88"就可以了。

Tomcat在设计的时候是对这种代理服务器和Tomcat集成的情况做了考虑,80端口之所以没问题是因为port为空,浏览器会默认走80端口,如果nginx这代理服务器不是80这个端口应该需要配置proxyPort的属性的,这样就不会遇到这个问题。

nginx负载均衡配置与tomcat+redis会话状态配置

nginx作为反向代理服务器可以用作负载均衡,可以用一台服务器作为负载均衡,上面安装nginx,另外用两台或者更多的服务器作为web应用服务器,上面安装相同的tomcat,nginx根据负载的相关策略将外网访问的请求分发到应用服务器上,每个请求都可能分发到不同的web应用服务器,所以需要处理一下tomcat的session问题,将所有的web应用服务器上tomcat的session统一存储在一个数据库里面比如redis,那么这里就需要一个数据库服务器,总体来要实现真正的负载均衡,至少需要4台服务器。

1.nginx负载均衡配置

nginx具体怎么安装前面有文章介绍。

负载均衡服务器ip:192.168.1.100

web应用服务器ip1:192.168.1.101

web应用服务器ip2:192.168.1.102

nginx.conf配置文件中配置

http {
    upstream tomcat {
         server 192.168.1.101:8080;
         server 192.168.1.102:8080;
    }

...

    server {
	listen       80;
        server_name  localhost;
...

        location / {
                proxy_pass http://tomcat;
                proxy_set_header   Host             $host;
                proxy_set_header   X-Real-IP        $remote_addr;
                proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        }
    }
...
}

重启nginx之后,可以访问地址192.168.1.100就能看到tomcat的欢迎页面,刷新页面会在两个web服务器间切换。

2.tomcat+redis会话状态配置

多个tomcat之间共享session,这样不管用户的请求发送到哪台服务器,用户的登陆状态都不会丢失了。

redis的tomcat session管理下载地址:https://github.com/jcoleman/tomcat-redis-session-manager

这个需要下载master分支的源代码下来然后编译,这个分支是tomcat7以上的版本,编译的依赖文件pom.xml如下

		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
			<version>2.2</version>
		</dependency>
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>2.5.2</version>
		</dependency>
		<dependency>
			<groupId>org.apache.tomcat</groupId>
			<artifactId>tomcat-catalina</artifactId>
			<version>7.0.27</version>
		</dependency>

编译完成之后将jar和依赖的两个jar包复制到tomcat的lib目录下:

image

jedis就是redis的开发库了,前面文章有介绍.假设redis安装在服务器192.163.168.1.99上,tomcat的配置文件context.xml中配置

<Context>


<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />
<Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"
         host="192.168.1.99"
         port="6379"
         database="0"
         maxInactiveInterval="60"/>

...

</Context>

重启tomcat那么配置就成功了。

总结

以上讲到的是基本的配置,nginx负载均衡上可以根据web应用服务器的硬件能力分配不同的负载策略以充分利用硬件资源,另外nginx可以配置静态文件的缓存功能,这样就不必每个请求都需要后台web服务器处理,静态文件直接由nginx从缓存中获取提高相应速度,缓存部分后续将继续研究。

作为后台的web应用,一定需要注意session的问题,用户的每个请求都会根据策略分发到不同的节点上,还有一些定时任务的处理,在单机的情况下很多定时任务都是放在web容器里面定时处理,在这种负载均衡的情况下一定要避免重复执行,或者将定时任务放到独立的一个服务器上执行。

最后负载均衡做起来之后,更新的时候可以停掉一台web服务器更新应用,另外一台照样提供服务,更新完一台服务器之后再更新另外一台服务器,整个系统看上去是没有停止的,这样就可以做到不间断服务的效果。

java调用linux命令(续)

之前文章有讲到使用方法Runtime.getRuntime().exec(command)执行操作系统的命令,在很多情况下是没有问题的,但是在某些环境下可能会导致执行的进程假死也就是挂起了,一直停在那里,其根本原因是命令行的输出流在某些情况下没有地方输出,导致命令一直等待输出,下面写个方法把命令的输出流读出来就解决了问题。

@SuppressWarnings("static-access")
public static int doWaitFor(Process process) {
  InputStream in = null;
  InputStream err = null;
  int exitValue = -1; // returned to caller when p is finished
  try {
    in = process.getInputStream();
    err = process.getErrorStream();
    boolean finished = false; // Set to true when p is finished
    while (!finished) {
      try {
        while (in.available() > 0) {
          // Print the output of our system call
          Character c = new Character((char) in.read());
          System.out.print(c);
        }
        while (err.available() > 0) {
          // Print the output of our system call
          Character c = new Character((char) err.read());
          System.out.print(c);
        }
        // Ask the process for its exitValue. If the process
        // is not finished, an IllegalThreadStateException
        // is thrown. If it is finished, we fall through and
        // the variable finished is set to true.
        exitValue = process.exitValue();
        finished = true;
      } catch (IllegalThreadStateException e) {
        // Process is not finished yet;
        // Sleep a little to save on CPU cycles
        Thread.currentThread().sleep(500);
      }
    }
  } catch (Exception e) {
    e.printStackTrace();
  } finally {
    try {
      if (in != null) {
        in.close();
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
    if (err != null) {
      try {
        err.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
  return exitValue;
}

 

在执行命令之后调用上面的方法获取命令行输出流:

Process process = Runtime.getRuntime().exec (“ls”);
doWaitFor(process);

Android Activity的生命周期

activity类处于android.app包中,继承体系如下:

1.java.lang.Object

2.android.content.Context

3.android.app.ApplicationContext

4.android.app.Activity

activity是单独的,用于处理用户操作。几乎所有的activity都要和用户打交道,所以activity类创建了一个窗口,开发人员可以通过setContentView(View)接口把UI放到activity创建的窗口上,当 activity指向全屏窗口时,也可以用其他方式实现:作为漂浮窗口(通过windowIsFloating的主题集合),或者嵌入到其他的 activity(使用ActivityGroup)。大部分的Activity子类都需要实现以下两个接口:

  • onCreate(Bundle)接口是初始化activity的地方. 在这儿通常可以调用setContentView(int)设置在资源文件中定义的UI, 使用findViewById(int) 可以获得UI中定义的窗口.
  • onPause()接口是使用者准备离开activity的地方,在这儿,任何的修改都应该被提交(通常用于ContentProvider保存数据).

为了能够使用Context.startActivity(),所有的activity类都必须在AndroidManifest.xml文件中定义有相关的“activity”项。

activity类是Android 应用生命周期的重要部分。

Activity生命周期

在系统中的Activity被一个Activity栈所管理。当一个新的Activity启动时,将被放置到栈顶,成为运行中的Activity,前一个Activity保留在栈中,不再放到前台,直到新的Activity退出为止。

Activity有四种本质区别的状态:

  1. 在屏幕的前台(Activity栈顶),叫做活动状态或者运行状态(active or running)
  2. 如果一个Activity失去焦点,但是依然可见(一个新的非全屏的Activity 或者一个透明的Activity 被放置在栈顶),叫做暂停状态(Paused)。一个暂停状态的Activity依然保持活力(保持所有的状态,成员信息,和窗口管理器保持连接),但是在系统内存极端低下的时候将被杀掉。
  3. 如果一个Activity被另外的Activity完全覆盖掉,叫做停止状态(Stopped)。它依然保持所有状态和成员信息,但是它不再可见,所以它的窗口被隐藏,当系统内存需要被用在其他地方的时候,Stopped的Activity将被杀掉。
  4. 如果一个Activity是Paused或者Stopped状态,系统可以将该Activity从内存中删除,Android系统采用两种方式进行删除,要么要求该Activity结束,要么直接杀掉它的进程。当该Activity再次显示给用户时,它必须重新开始和重置前面的状态。

下面的图显示了Activity的重要状态转换,矩形框表明Activity在状态转换之间的回调接口,开发人员可以重载实现以便执行相关代码,带有颜色的椭圆形表明Activity所处的状态。

activity

在上图中,Activity有三个关键的循环:

  1. 整个的生命周期,从onCreate(Bundle)开始到onDestroy()结束。Activity在onCreate()设置所有的“全局”状态,在onDestory()释放所有的资源。例如:某个Activity有一个在后台运行的线程,用于从网络下载数据,则该Activity可以在onCreate()中创建线程,在onDestory()中停止线程。
  2. 可见的生命周期,从onStart()开始到onStop()结束。在这段时间,可以看到Activity在屏幕上,尽管有可能不在前台,不能和用户交互。在这两个接口之间,需要保持显示给用户的UI数据和资源等,例如:可以在onStart中注册一个IntentReceiver来监听数据变化导致UI的变动,当不再需要显示时候,可以在onStop()中注销它。onStart(),onStop()都可以被多次调用,因为Activity随时可以在可见和隐藏之间转换。
  3. 前台的生命周期,从onResume()开始到onPause()结束。在这段时间里,该Activity处于所有 Activity的最前面,和用户进行交互。Activity可以经常性地在resumed和paused状态之间切换,例如:当设备准备休眠时,当一个 Activity处理结果被分发时,当一个新的Intent被分发时。所以在这些接口方法中的代码应该属于非常轻量级的。

Activity的整个生命周期都定义在下面的接口方法中,所有方法都可以被重载。所有的Activity都需要实现 onCreate(Bundle)去初始化设置,大部分Activity需要实现onPause()去提交更改过的数据,当前大部分的Activity也需要实现onFreeze()接口,以便恢复在onCreate(Bundle)里面设置的状态。

public class Activity extends ApplicationContext {
	protected void onCreate(Bundle icicle);
	protected void onStart();
	protected void onRestart();
	protected void onResume();
	protected void onFreeze(Bundle outIcicle);
	protected void onPause();
	protected void onStop();
	protected void onDestroy();

}

android使用ACTION_SEND分享内容

几乎是所有的app现在都有分享的功能,分享一些文本,链接和图片之类的到各大社交平台,那么这么一个简单的功能是如何实现的能,android里面有一个默认的接口,我们来看看怎么使用。

android默认的intent ACTION_SEND可以实现这个分享的功能,看看下面的示例

Intent sharingIntent = new Intent(Intent.ACTION_SEND);
sharingIntent.setType("text/html");
sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, Html.fromHtml("<p>This is the text that will be shared.</p>"));
startActivity(Intent.createChooser(sharingIntent,"Share using"));

以上是分享一段html,也是最常用的分享方式,也可以分享一些图片如下

Intent sharingIntent = new Intent(Intent.ACTION_SEND);
Uri screenshotUri = Uri.parse(path);

sharingIntent.setType("image/png");
sharingIntent.putExtra(Intent.EXTRA_STREAM, screenshotUri);
startActivity(Intent.createChooser(sharingIntent, "Share image using"));

分享多个图片

ArrayList<Uri> imageUris = new ArrayList<Uri>();
imageUris.add(imageUri1); // Add your image URIs here
imageUris.add(imageUri2);

Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND_MULTIPLE);
shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, imageUris);
shareIntent.setType("image/*");
startActivity(Intent.createChooser(shareIntent, "Share images to.."));

上面的分享当然是在手机上有可以分享的软件才能接受到分享的内容,以上的分享事件都会打开一个选择分享软件的列表,比如微信或者微博之类的,用户选择一个之后发送分享内容,那么下面这段xml可以让你的app可以出现在分享列表中,以实现分享的效果:

<activity android:name=".ui.MyActivity" >
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="image/*" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="text/plain" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.SEND_MULTIPLE" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="image/*" />
    </intent-filter>
</activity>

下面这段代码是处理接收到消息之后的基本过程

void onCreate (Bundle savedInstanceState) {
    ...
    // Get intent, action and MIME type
    Intent intent = getIntent();
    String action = intent.getAction();
    String type = intent.getType();

    if (Intent.ACTION_SEND.equals(action) && type != null) {
        if ("text/plain".equals(type)) {
            handleSendText(intent); // Handle text being sent
        } else if (type.startsWith("image/")) {
            handleSendImage(intent); // Handle single image being sent
        }
    } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null) {
        if (type.startsWith("image/")) {
            handleSendMultipleImages(intent); // Handle multiple images being sent
        }
    } else {
        // Handle other intents, such as being started from the home screen
    }
    ...
}

void handleSendText(Intent intent) {
    String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
    if (sharedText != null) {
        // Update UI to reflect text being shared
    }
}

void handleSendImage(Intent intent) {
    Uri imageUri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
    if (imageUri != null) {
        // Update UI to reflect image being shared
    }
}

void handleSendMultipleImages(Intent intent) {
    ArrayList<Uri> imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
    if (imageUris != null) {
        // Update UI to reflect multiple images being shared
    }
}

总结

android自带的分享只是列出接收分享内容的app列表,由用户选择启动对应的app进行分享,当系统中没有安装目标app的时候是没办法分享的,那么对一般的应用来说这就已经足够了,系统自带的功能一般都是比较基本的东西,另一个办法是使用第三方分享sdk可以实现更加复杂的分享功能。

java导出可执行jar包与启动脚本

java写桌面程序的时候如何启动有很多种办法,有些打包工具就可以直接生成一个快捷方式启动可执行的jar包,也可以直接写一个启动脚本启动,我们来看看启动脚本是怎么写的。

1.导出可执行jar包,首先需要创建一个运行配置

image

2.起个名字并选择需要导出的项目以及启动的时候的入口函数main所在的类

image

3.在项目上右键->导出,选择可执行jar包

image

4.选择上面配置好的运行配置,选择导出的路径

image

这样就导出成功了,依赖的jar包会导到一个文件夹里

5.写个脚本,并制定虚拟机路径

示例脚本如下

@cls
set JRE_HOME=%cd%\jre6
set JAVA_HOME=%cd%\jre6
PATH=%JRE_HOME%\bin;%PATH%

set CLASSPATH = %JRE_HOME%\lib\rt.jar;

@echo %JRE_HOME%
start javaw -jar myapp.jar

把依赖的jar包放到可执行jar包同级的lib目录里面就可以了。

maven依赖war包创建项目

对于bs结构的项目,一般公司都是会有一个适合自己的通用框架,项目在此基础上添加业务功能,那么基础框架可以发布成一个独立的war包,其他项目都依赖与这个包来创建,当版本更新的时候就可以很方便的升级基础框架的东西。

下面来看一下创建的过程

1.创建maven项目

image

2.选择简单项目

image

3.输入项目的信息,和依赖的war包的信息

image

这样项目就创建完成了

打开创建好之后的项目里的文件pom.xml

可以看到依赖项如下

  <parent>
    <groupId>fullstacks</groupId>
    <artifactId>fullstacks.parent</artifactId>
    <version>1.0.0</version>
  </parent>

这样如果war包没有问题的话,就可以运行起来了,当然先配好tomcat

4.编译

image

第一次编译的时候会下载war包的内容,并生成目标文件

在项目的根目录下生成一个目录叫target,里面有下载的war包以及和新项目加入的文件,如果新项目里面有相同路径的文件则会覆盖掉依赖的内容,这样就形成了新的项目。如果想覆盖掉基础框架的文件比如js,html和资源文件之类的,只需要在相同的路径建立相同的文件就可以了。

linux下apache ftpserver的安装配置

apache-ftpd安装过程非常简单,但是需要做一些配置,一个是用户的配置另外一个就是端口的配置,在防火墙iptables打开的情况需要配置两个端口一个用与ftp连接一个用于ftp数据的传输。

从官方网站下载http://mina.apache.org/ftpserver-project/downloads.html ,下载linux包ftpserver-1.0.6.tar.gz

1.安装文件

比如安装到路径/usr/local/apache-ftpd

tar zxvf ftpserver-1.0.6.tar.gz
cp ftpserver-1.0.6 /usr/local/apache-ftpd

2.配置用户

编辑文件res/conf/user.properties

vi res/conf/user.properties

配置用户admin并设置ftp保存的文件路径,注意对目录的可写权限是writepermission=true,要不然不能上传文件

# Password is "admin"
ftpserver.user.admin.userpassword=21232F297A57A5A743894A0E4A801FC3
ftpserver.user.admin.homedirectory=/usr/local/ftp/
ftpserver.user.admin.enableflag=true
ftpserver.user.admin.writepermission=true
ftpserver.user.admin.maxloginnumber=0
ftpserver.user.admin.maxloginperip=0
ftpserver.user.admin.idletime=0
ftpserver.user.admin.uploadrate=0
ftpserver.user.admin.downloadrate=0

注意这里的ftp文件路径是/usr/local/ftp/,需要创建这个目录

mkdir /usr/local/ftp

3.配置连接端口

编辑文件res/conf/ftpd-typical.xml

vi res/conf/ftpd-typical.xml

在listeners节点下加入数据监听接口

<listeners>
	<nio-listener name="default" port="2121">
		<ssl>
			<keystore file="./res/ftpserver.jks" password="password" />
		</ssl>
		<data-connection idle-timeout="30">
		<active local-port="2120"/>
		<passive ports="2120" />
		</data-connection>
	</nio-listener>
</listeners>

这里配置的端口是2121和2120,所以防火墙iptables需要开启这两个端口其他机器才能访问这个ftp的2121端口

4.创建启动脚本并配置位开机启动

在bin目录下创建start.sh脚本,内容如下

/usr/local/apache-ftpd/bin/ftpd res/conf/ftpd-typical.xml &

保存并设置脚本可执行的权限

chmod +x bin/start.sh

开机启动,将启动脚本加入到开机执行脚本中

 vi /etc/rc.local

在文件中加入下面一行

/usr/local/apache-ftpd/bin/start.sh

这样开机的时候就会自动启动ftp服务了

也可以执行start.sh启动ftp 如

bin/start.sh

到此安装过程全部结束,如果想停止ftp服务只能通过ps找到进程将进程杀死.

以上配置完成之后ftp访问路径ftp://admin:admin@ip:2121/ 可以在浏览器地址栏输入进行验证ftp服务是否已经启动好。

spring mvc下载文件简单实现

文件下载有好多种方式,通过返回值,通过servlet的返回输出流等,我们这里使用spring的controller简单实现如何下载文件,并从url传入参数。

这是controller的简单实现,使用aspose.cells解析excel模板,并保存到输出流就可以下载了。

/**
* 下载文件,参数可以通过url传入
*/
@RequestMapping(value="/download/{fileName}.xls")
public void download(@RequestParam(required=false,value="arg1") String arg1,HttpServletRequest request, HttpServletResponse response){
	
	//解析excel模板
	WorkbookDesigner designer = new WorkbookDesigner();  
	String template_file_path = "d:/aspose/cell_sample.xls";  
	Workbook wb = new Workbook(template_file_path); 
	designer.setWorkbook(wb);
	
	//解析数据
	//designer.setDataSource("list", new MapData(getHashMapList()));//map list作为数据源
	//designer.process();//全自动赋值
	
	//文件输出到 输出流
	wb.save(response.getOutputStream(), SaveFormat.EXCEL_97_TO_2003);
	
}

在前端的脚本中直接使用url下载代码如下

window.location = escape("/download/myfile.xls?arg1=val1");

以上简易实现文件下载,注意如果是url传入中文的话,tomcat必须配置url的编码模式为utf-8,要不然的话在后台取到的参数会可能是乱码的。

java调用linux命令

Java可以直接调用Linux命令,形式如下:
Runtime.getRuntime().exec(command)
举例:运行ls,top命令可以这样:
Runtime.getRuntime().exec(“ls”);
但是这样执行时没有任何输出,原因:
调用Runtime.exec方法将产生一个本地的进程,并返回一个Process子类的实例,
(注意:Runtime.getRuntime().exec(command)返回的是一个Process类的实例),
该实例可用于控制进程或取得进程的相关信息. 由于调用Runtime.exec方法所创建的子进程没有自己的终端或控制台,因此该子进程的标准IO(如stdin,stdou,stderr)都通过Process.getOutputStream(),Process.getInputStream(), Process.getErrorStream()方法重定向给它的父进程了.用户需要用这些stream来向子进程输入数据或获取子进程的输出. 可以采用如下方法:
try
{
Process process = Runtime.getRuntime().exec (“ls”);
InputStreamReader ir=new InputStreamReader(process.getInputStream());
LineNumberReader input = new LineNumberReader (ir);
String line;
while ((line = input.readLine ()) != null){
System.out.println(line)
}
catch (java.io.IOException e){
System.err.println (“IOException ” + e.getMessage());
}

aspose-word for java使用map作为数据源

aspose对word的操作和对excel的操作是分为两个独立的jar包实现的,同样的word模板的使用也是非常之方便,依然很遗憾默认并没有实现使用map数据作为数据源,这里我们来实现数据源接口IMailMergeDataSource来提供map数据源。

1.word模板,使用MergeFeild绑定数据

在word模板中通过菜单”插入→文档部件→域”插入MergeField域

1 2

根据上面插入域的方法,我们来创建一个简单的模板,使用一个map对象和一个map的列表

 

12

注意一个区域是以TableStart:和TableEnd:结束,冒号后面是数据源的名字.

2.实现IMailMergeDataSource接口提供map数据源

/**
 * 实现对HashMap的支持
 */
public class MapData implements IMailMergeDataSource  {

	@SuppressWarnings("rawtypes")
	private List<Map> dataList;

	private int index;

	// word模板中的«TableStart:tableName»«TableEnd:tableName»对应
	private String tableName = null;

	/**
	 * @param dataList
	 *            数据集
	 * @param tableName
	 *            与模板中的Name对应
	 */
	@SuppressWarnings("rawtypes")
	public MapData(List<Map> dataList, String tableName) {
		this.dataList = dataList;
		this.tableName = tableName;
		index = -1;
	}

	/**
	 * @param data
	 *            单个数据集
	 * @param tableName
	 *            与模板中的Name对应
	 */
	@SuppressWarnings("rawtypes")
	public MapData(Map data, String tableName) {
		if (this.dataList == null) {
			this.dataList = new ArrayList<Map>();
			this.dataList.add(data);
		}
		this.tableName = tableName;
		index = -1;
	}

	/**
	 * 获取结果集总数
	 * 
	 * @return
	 */
	private int getCount() {
		return this.dataList.size();
	}

	@Override
	public IMailMergeDataSource getChildDataSource(String arg0)
			throws Exception {
		return null;
	}

	@Override
	public String getTableName() throws Exception {
		return this.tableName;
	}

	/**
	 * 实现接口 获取当前index指向数据行的数据 将数据存入args数组中即可
	 * 
	 * @return ***返回false则不绑定数据***
	 */
	@Override
	public boolean getValue(String key, Object[] args) throws Exception {
		if (index < 0 || index >= this.getCount()) {
			return false;
		}
		if (args != null && args.length > 0) {
			args[0] = this.dataList.get(index).get(key);
			return true;
		} else {
			return false;
		}
	}

	/**
	 * 实现接口 判断是否还有下一条记录
	 */
	@Override
	public boolean moveNext() throws Exception {
		index += 1;
		if (index >= this.getCount()) {
			return false;
		}
		return true;
	}


}

3.实现IMailMergeDataSourceRoot接口以提供多个数据源

/**
 * 提供多个数据源
 * @author Administrator
 *
 */
public class MapDataSet implements  IMailMergeDataSourceRoot {

	/**
	 * 多个数据源
	 */
	private Map<String,IMailMergeDataSource> ds = new HashMap<String,IMailMergeDataSource>();
	
	public MapDataSet(){}

	@Override
	public IMailMergeDataSource getDataSource(String arg0) throws Exception {
		if(this.ds.containsKey(arg0)){
			return this.ds.get(arg0);
		}
		return null;
	}
	/**
	 * 添加一个数据源
	 * @param tableName
	 * @param data
	 */
	public void add(IMailMergeDataSource data){
		try {
			this.ds.put(data.getTableName(), data);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

4.使用实现好的数据源解析word模板

	public static void main(String[] args) throws Exception{
		String template_file_path = "d:/aspose/word_sample.doc";  
		Document doc = new Document(template_file_path);

		//两个数据源
		MapDataSet ds = new MapDataSet();
		ds.add( new MapData(getHashMapData(),"map"));
		ds.add( new MapData(getHashMapList(),"list"));
		
		//主从数据源
		/*
		DataSet dataset = new DataSet();
		DataTable dt1 = mapListToDataTable(getHashMapList(),"dt1");
		DataTable dt2 = mapListToDataTable(getHashMapList(),"dt2");
		dataset.getTables().add(dt1);
		dataset.getTables().add(dt2);
		dataset.getRelations().add(new DataRelation("name_key",dt1.getColumns().get("name"), dt2.getColumns().get("name")));
		doc.getMailMerge().executeWithRegions(dataset);
		*/
		//没有数据的模板清楚掉
		doc.getMailMerge().setCleanupOptions(MailMergeCleanupOptions.REMOVE_UNUSED_REGIONS);
		//自动插入数据
		doc.getMailMerge().executeWithRegions(ds);
		
		doc.save("d:/aspose/word_sample_i.doc");
		
		System.out.println("ok!");
	}
	
	/**
	 * 单个数据
	 * @return
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private static HashMap getHashMapData(){
		
		HashMap data = new HashMap();
		
		data.put("name", "fullstacks");
		data.put("age", "30");
		
		return data;
		
	}
	
	/**
	 * hashMap 列表
	 * @return
	 */
	@SuppressWarnings("rawtypes")
	private static List<Map> getHashMapList(){
		 List<Map> datas= new ArrayList<Map>();
		 datas.add(getHashMapData());
		 datas.add(getHashMapData());
		 datas.add(getHashMapData());
		 datas.add(getHashMapData());
		 return datas; 
	}

以上代码执行之后得到的效果如下

13

5.使用主从关系的列表

创建一个word模板如下

14

使用DataSet创建主从关系的数据结构,这里提供一个将Map列表转换为一个DataTable

	/**
	 * map列表转换为datatable
	 * @param data
	 * @return
	 */
	@SuppressWarnings("rawtypes")
	public static DataTable mapListToDataTable(List<Map> datas,String tableName){
		if(datas == null || datas.size() == 0)return null;
		DataTable dt = new DataTable(tableName);
		Map fistrow = datas.get(0);
		for(Object key: fistrow.keySet()){
			dt.getColumns().add(new DataColumn((String)key,fistrow.get(key).getClass()));			
		}
		for(Map item:datas){
			DataRow dr = dt.newRow();
			for(Object key: item.keySet()){
				dr.set((String)key, item.get(key));
			}
			try {
				dt.getRows().add(dr);
			} catch (ConstraintException e) {
				e.printStackTrace();
			} catch (InvalidConstraintException e) {
				e.printStackTrace();
			}
		}	
		return dt;
	}

构建DataSet数据源

		DataSet dataset = new DataSet();
		DataTable dt1 = mapListToDataTable(getHashMapList(),"dt1");
		DataTable dt2 = mapListToDataTable(getHashMapList(),"dt2");
		dataset.getTables().add(dt1);
		dataset.getTables().add(dt2);
		dataset.getRelations().add(new DataRelation("name_key",dt1.getColumns().get("name"), dt2.getColumns().get("name")));
		doc.getMailMerge().executeWithRegions(dataset);

得到的效果如下

15

总结

以上是实现了一个通用的数据结构map作为aspose word的数据源,如果是结合spring jdbc的话,从数据库中查询出来的结果数据就是一个hashmap的list,这样就可以使用了,不必要转换为datatable那么麻烦,然后如果是多个数据源的话,就可以使用上面实现的MapDataSet实现,已提供多个数据源一次性把word模板解析完毕,如果是主从表的结构数据只能通过自带DataSet来完成了,上面已提供了一个静态方法方便把map列表转换为datatable,如果数据量比较大的话这样遍历可能效率低下,可以直接使用dataredder作为数据源,这样效率更高。

aspose-cells for java使用map作为数据源

使用excel作为模板导出数据是很普遍的应用,而且非常方便,aspose-cells是这样一个商业的软件,使用非常之方便,几行代码就可以搞定将数据自动插入到excel模板中,但是默认的并没有实现以map作为数据源,只是提供了一个借口ICellsDataTable,下面我们来实现这个接口。

1.实现接口ICellsDataTable支持Map的数据

/**
 * 使用hashmap作为数据源
 * @author Administrator
 *
 */
public class MapData implements ICellsDataTable {
	//数据集合  
    @SuppressWarnings("rawtypes")
	private List<Map> dataList = null;  

    //索引  
    private int index;  
      
    //存放dataList当中Map<String, Object>的key  
    private String[] columns = null;  
  
    @SuppressWarnings("rawtypes")
	public MapData(Map data) {  
        if(this.dataList == null) {  
            this.dataList = new ArrayList<Map>();  
        }  
        dataList.add(data);
    }  
      
    @SuppressWarnings("rawtypes")
	public MapData(List<Map> data) {  
        this.dataList = data;  
    }  
      
    /** 
     * 初始化方法 
     */  
    public void beforeFirst() {  
        index = -1;  
        columns = this.getColumns();  
    }  
  
    /** 
     * WorkbookDesigner自动调用 
     * 会将this.getColumns()方法所返回的列 按照顺序调用改方法 
     */  
    @SuppressWarnings("rawtypes")
	public Object get(int columnIndex) {  
        if(index < 0 || index >= this.getCount()) {  
            return null;  
        }  
        Map record = this.dataList.get(index);  
        String columnName = this.columns[columnIndex];  
        return record.get(columnName);  
    }  
  
    /** 
     * 根据columnName返回数据 
     */  
    @SuppressWarnings("rawtypes")
	public Object get(String columnName) {  
        Map record = this.dataList.get(index);  
        return record.get(columnName);  
    }  
  
    /** 
     * 获得列集合 
     */  
    @SuppressWarnings({ "rawtypes", "unchecked" })
	public String[] getColumns() {  
        Map temp = this.dataList.get(0);  
        Set<Entry> entrys = temp.entrySet();  
        List<String> columns = new ArrayList<String>();  
        for (Entry e : entrys) {  
            columns.add((String)e.getKey());  
        }  
        String[] s = new String[entrys.size()];  
        columns.toArray(s);  
        return s;  
    }  
  
    public int getCount() {  
        return this.dataList.size();  
    }  
  
    public boolean next() {  
        index += 1;  
        if(index >= this.getCount())  
        {  
            return false;  
        }  
        return true;  
    }  
  
}

2.使用的示例代码

我们来创建一个简单的excel模板,如下图所示

image

下面的代码将数据插入到这个模板中,这里我们做了两个数据源,分别命名为map和list,在模板的单元格中以&=开始加上数据源的名字加个点后面跟map里面的key名。格式就是&=name.key

public class ExcelUtils {

	public static void main(String[] args) throws Exception{		
		WorkbookDesigner designer = new WorkbookDesigner();  
		String template_file_path = "d:/aspose/cell_sample.xls";  
		Workbook wb = new Workbook(template_file_path); 
		designer.setWorkbook(wb);
				
		//给模板对象设置数据源  
		designer.setDataSource("map", new MapData(getHashMapData()));//hashmap对象作为数据源		
		//设置列表数据
		designer.setDataSource("list", new MapData(getHashMapList()));//map list作为数据源
		
		designer.process();//全自动赋值
		
		wb.save("d:/aspose/cell_sample_i.xls");//可以保存到数据流里面,以便下载使用
		System.out.println("ok!");
	}
	
	/**
	 * 单个数据
	 * @return
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private static HashMap getHashMapData(){
		
		HashMap data = new HashMap();
		
		data.put("name", "fullstacks");
		data.put("age", "30");
		
		return data;
		
	}
	
	/**
	 * hashMap 列表
	 * @return
	 */
	@SuppressWarnings("rawtypes")
	private static List<Map> getHashMapList(){
		 List<Map> datas= new ArrayList<Map>();
		 datas.add(getHashMapData());
		 datas.add(getHashMapData());
		 datas.add(getHashMapData());
		 datas.add(getHashMapData());
		 return datas; 
	}
	
}

上面的模板插入数据之后的效果如下

image

以上就是完美使用map作为数据源的实现,当然也可以实现以其他数据作为数据源,java bean作为数据源是直接支持的了。

总结

商业的软件使用起来确实是非常方便,几行代码的事情,使用一些开源的东西可能稍微要写一些复杂的代码,那么使用excel模板一个最大的好处就是能够在模板里面吧excel格式和样式都配好,包括一些统计等等,只需要插入数据就可以了。

Eclipse+Maven热部署调试

Eclipse JEE原生方式(WTP)调试Web应用的时候,当修改java类的时候,tomcat会自动重启,大部分的人都处于这种状态中,改点东西然后等待重启几十秒的时间,这是在浪费生命,修改资源文件不需要重启,其实java本身已经支持hot code replace热部署,以下是简单的配置。

maven必须使用tomcat插件

<plugin>
	<groupId>org.apache.tomcat.maven</groupId>
	<artifactId>tomcat7-maven-plugin</artifactId>
	<version>2.2</version>
</plugin>

eclipse中tomcat的server.xml的配置

image

文件中找到你项目的Context配置,设置reloadable=”false” 就可以了,默认是ture。

<Context docBase="stapp.web" path="/stapp.web" reloadable="false" source="org.eclipse.jst.jee.server:stapp.web"/>

修改完毕重启tomcat,然后修改java代码并保存,发现tomcat不在自动重启了,页面访问正常并执行了修改之后的代码,断点在新的代码上也没问题。

java 基础教程hello world

Java是完全面向对象的语言。Java通过虚拟机的运行机制,实现“跨平台”的理念。我在这里想要呈现一个适合初学者的教程,希望对大家有用。

“Hello World!”

先来看一个HelloWorld.java程序。这个程序在屏幕上打印出一串字符”Hello World!”:

public class HelloWorld
{
    public static void main(String[] args)
    {
        System.out.println("Hello World!");
    }
}

程序中包括Java的一些基本特征:

  • 类(class):上面程序定义了一个类HelloWorld,该类的名字与.java文件的名字相同。
  • 方法(method):类的内部定义了该类的一个方法main。
  • 语句(statement):真正的“打印”功能由一个语句实现,即: System.out.println(“Hello World!”);

 

下面两点有关Java的书写方式:

  • Java中的语句要以;结尾 (与C/C++相同)。
  • 用花括号{}来整合语句,形成程序块。通过程序块,我们可以知道程序的不同部分的范围,比如类从哪里开始,到哪里结束。

编译与运行

Java程序要经过编译器编译才能执行。在Linux或Mac下,可以下载安装Java JDK

使用javac来编译。在命令行中输入下面语句编译:

$javac HelloWorld.java

当前路径下,将有一个名为HelloWorld.class的文件生成。

使用java命令来运行。Java会搜寻该类中的main方法,并执行。

$java HelloWorld

 

变量

计算机语言通常需要在内存中存放数据,比如C语言中的变量,Java也有类似的变量。Java和C语言都是静态类型的语言。在使用变量之前,要声明变量的类型。

变量(variable)占据一定的内存空间。不同类型的变量占据不同的大小。Java中的变量类型如下:

存储大小     例值     注释

byte      1byte        3      字节

int       4bytes       3      整数

short     2bytes       3      短整数

long      8bytes       3      长整数

float     4bytes     1.2      单精度浮点数

double    8bytes     1.2      双精度浮点数

char      2bytes     ‘a’      字符

boolean   1bit      true      布尔值

 

在Java中,变量需要先声明(declare)才能使用。在声明中,我说明变量的类型,赋予变量以特别名字,以便在后面的程序中调用它。你可以在程序中的任意位置声明变量。 比如:

public class Test
{
    public static void main(String[] args)
    {
        System.out.println("Declare in the middle:");
        int a;
        a = 5;
        System.out.println(a);  // print an integer
    }
}

上面a是变量名。可以在声明变量的同时,给变量赋值,比如 int a = 5;

*** “变量”的概念实际上来自于面向过程的编程语言。在Java中,所谓的变量实际上是“基本类型” (premitive type)。我们将在类的讲解中更多深入。

 

上面的程序还可以看到,Java中,可用//引领注释。

 

数组

Java中有数组(array)。数组包含相同类型的多个数据。我用下面方法来声明一个整数数组:

int[] a;

 

在声明数组时,数组所需的空间并没有真正分配给数组。我可以在声明的同时,用new来创建数组所需空间:

int[] a = new int[100];

这里创建了可以容纳100个整数的数组。相应的内存分配也完成了。

 

我还可以在声明的同时,给数组赋值。数组的大小也同时确定。

int[] a = new int[] {1, 3, 5, 7, 9};

使用int[i]来调用数组的i下标元素。i从0开始。

其他类型的数组与整数数组相似。

 

表达式

表达式是变量、常量和运算符的组合,它表示一个数据。1 + 1是常见的表达式。再比如:

public class Test
{
    public static void main(String[] args)
    {
        System.out.println("Declare in the middle:");
        int a;
        a = 5 + 1;
        System.out.println(a);  // print an integer
    }
}

上面的5 + 1也是一个表达式,等于6。

 

数学表达式

数学运算,结果为一个数值

1 + 2                  加法

4 – 3.4                减法

7 * 1.5                乘法

3.5 / 7                除法

7 % 2                  求余数

 

关系表达式

判断表达式是否成立。即一个boolean值,真假

a > 4.2                大于

3.4 >= b               大于等于

1.5 < 9                小于

6 <= 1                 小于等于

2 == 2                 等于

2 != 2                 不等于

 

布林表达式

两个boolean值的与、或、非的逻辑关系

true && false          and

(3 > 1) || (2 == 1)    or

!true                  not

 

位运算

对整数的二进制形式逐位进行逻辑运算,得到一个整数

&                      and

|                      or

^                      xor

~                      not

5 << 3                 0b101 left shift 3 bits

6 >> 1                 0b110 right shift 1 bit

 

还有下列在C中常见的运算符,我会在用到的时候进一步解释:

m++                    变量m加1

n–                    变量n减1

condition ? x1 : x2   condition为一个boolean值。根据condition,取x1或x2的值

 

控制结构

Java中控制结构(control flow)的语法与C类似。它们都使用{}来表达隶属关系。

 

选择 (if)

if (conditon1) {
    statements;
    ...
}
else if (condition2) {
    statements;
    ...
}
else {
    statements;
    ...
}

上面的condition是一个表示真假值的表达式。statements;是语句。

 

循环 (while)

while (condition) {

    statements;

}

 

循环 (do… while)

do {

    statements;

} while(condition);  // 注意结尾的;

 

循环 (for)

for (initial; condition; update) {

    statements;

}

 

跳过或跳出循环

在循环中,可以使用

break; // 跳出循环

continue; // 直接进入下一环

 

练习 写一个Java程序,计算从1加2,加3…… 一直加到999的总和

 

选择 (switch)

switch(expression) {

    case 1:

        statements;

        break;

    case 2:

        statements;

        break;

    …

    default:

        statements;

        break;

}

OAuth 简介

OAuth 协议致力于使网站和应用程序(统称为消费方 Consumer)能够在无须用户透露其认证信息的情况下,通过 API 访问该用户在服务提供方(Service Provider)那里的受保护资源。更一般地说,OAuth 为 API 认证提供了一个可自由实现且通用的方法。目前互联网很多服务如 Open API 等都提供了 OAuth 认证服务,OAuth 标准也逐渐成为开放资源授权的标准。本文讨论如何使用 Google Code 上提供的 OAuth Java 库来实现基于 OAuth 认证的 Java 应用,并结合 Google 的 Data Service,给出使用 OAuth 方式访问 Google Data 的例子。

OAuth 简介

OAuth 是由 Blaine Cook、Chris Messina、Larry Halff 及 David Recordon 共同发起的,目的在于为 API 访问授权提供一个安全、开放的标准。

基于 OAuth 认证授权具有以下特点:

  • 安全。OAuth 与别的授权方式不同之处在于:OAuth 的授权不会使消费方(Consumer)触及到用户的帐号信息(如用户名与密码),也是是说,消费方无需使用用户的用户名与密码就可以申请获得该用户资源的授权。
  • 开放。任何消费方都可以使用 OAuth 认证服务,任何服务提供方 (Service Provider) 都可以实现自身的 OAuth 认证服务。
  • 简单。不管是消费方还是服务提供方,都很容易于理解与使用。

OAuth 的解决方案如下图所示。

图 1. OAuth Solution

图 1. OAuth Solution

如图 1 所示 OAuth 解决方案中用户、消费方及其服务提供方之间的三角关系:当用户需要 Consumer 为其提供某种服务时,该服务涉及到需要从服务提供方那里获取该用户的保护资源。OAuth 保证:只有在用户显式授权的情况下(步骤 4),消费方才可以获取该用户的资源,并用来服务于该用户。

从宏观层次来看,OAuth 按以下方式工作:

  1. 消费方与不同的服务提供方建立了关系。
  2. 消费方共享一个密码短语或者是公钥给服务提供方,服务提供方使用该公钥来确认消费方的身份。
  3. 消费方根据服务提供方将用户重定向到登录页面。
  4. 该用户登录后告诉服务提供方该消费方访问他的保护资源是没问题的。

OAuth 认证授权流程

在了解 OAuth 认证流程之前,我们先来了解一下 OAuth 协议的一些基本术语定义:

  • Consumer Key:消费方对于服务提供方的身份唯一标识。
  • Consumer Secret:用来确认消费方对于 Consumer Key 的拥有关系。
  • Request Token:获得用户授权的请求令牌,用于交换 Access Token。
  • Access Token:用于获得用户在服务提供方的受保护资源。
  • Token Secret:用来确认消费方对于令牌(Request Token 和 Access Token)的拥有关系。

图 2. OAuth 授权流程(摘自 OAuth 规范)

图 2. OAuth 授权流程(摘自 OAuth 规范)

对于图 2 具体每一执行步骤,解释如下:

  • 消费方向 OAuth 服务提供方请求未授权的 Request Token。
  • OAuth 服务提供方在验证了消费方的合法请求后,向其颁发未经用户授权的 Request Token 及其相对应的 Token Secret。
  • 消费方使用得到的 Request Token,通过 URL 引导用户到服务提供方那里,这一步应该是浏览器的行为。接下来,用户可以通过输入在服务提供方的用户名 / 密码信息,授权该请求。一旦授权成功,转到下一步。
  • 服务提供方通过 URL 引导用户重新回到消费方那里,这一步也是浏览器的行为。
  • 在获得授权的 Request Token 后,消费方使用授权的 Request Token 从服务提供方那里换取 Access Token。
  • OAuth 服务提供方同意消费方的请求,并向其颁发 Access Token 及其对应的 Token Secret。
  • 消费方使用上一步返回的 Access Token 访问用户授权的资源。

总的来讲,在 OAuth 的技术体系里,服务提供方需要提供如下基本的功能:

  • 第 1、实现三个 Service endpoints,即:提供用于获取未授权的 Request Token 服务地址,获取用户授权的 Request Token 服务地址,以及使用授权的 Request Token 换取 Access Token 的服务地址。
  • 第 2、提供基于 Form 的用户认证,以便于用户可以登录服务提供方做出授权。
  • 第 3、授权的管理,比如用户可以在任何时候撤销已经做出的授权。

而对于消费方而言,需要如下的基本功能:

  • 第 1、从服务提供方获取 Customer Key/Customer Secret。
  • 第 2、提供与服务提供方之间基于 HTTP 的通信机制,以换取相关的令牌。

我们具体来看一个使用 OAuth 认证的例子。

在传统的网站应用中,如果您想在网站 A 导入网站 B 的联系人列表,需要在网站 A 输入您网站 B 的用户名、密码信息。例如,您登陆 Plaxo (https://www.plaxo.com ),一个联系人管理网站,当您想把 GMail 的联系人列表导入到 Plaxo,您需要输入您的 GMail 用户名 / 密码,如图 3 所示:

图 3. 在 Plaxo 获得 GMail 联系人

图 3. 在 Plaxo 获得 GMail 联系人

在这里,Plaxo 承诺不会保存您在 Gmail 的密码。

如果使用 OAuth 认证,情况是不同的,您不需要向网站 A(扮演 Consumer 角色)暴露您网站 B(扮演 Service Provider 角色)的用户名、密码信息。例如,您登录 http://lab.madgex.com/oauth-net/googlecontacts/default.aspx 网站, 如图 4 所示:

图 4. 在 lab.madgex.com 获得 GMail 联系人

图 4. 在 lab.madgex.com 获得 GMail 联系人

点击“Get my Google Contacts”,浏览器将会重定向到 Google,引导您登录 Google,如图 5 所示:

图 5. 登录 Google

图 5. 登录 Google

登录成功后,将会看到图 6 的信息:

图 6. Google 对 lab.madgex.com 网站授权

图 6. Google 对 lab.madgex.com 网站授权

在您登录 Google,点击“Grant access”,授权 lab.madgex.com 后,lab.madgex.com 就能获得您在 Google 的联系人列表。

在上面的的例子中,网站 lab.madgex.com 扮演着 Consumer 的角色,而 Google 是 Service Provider,lab.madgex.com 使用基于 OAuth 的认证方式从 Google 获得联系人列表。

下一节,本文会给出一个消费方实现的例子,通过 OAuth 机制请求 Google Service Provider 的 OAuth Access Token,并使用该 Access Token 访问用户的在 Google 上的日历信息 (Calendar)。

示例

准备工作

作为消费方,首先需要访问 https://www.google.com/accounts/ManageDomains,从 Google 那里获得标志我们身份的 Customer Key 及其 Customer Secret。另外,您可以生成自己的自签名 X509 数字证书,并且把证书上传给 Google,Google 将会使用证书的公钥来验证任何来自您的请求。

具体的操作步骤,请读者参考 Google 的说明文档:http://code.google.com/apis/gdata/articles/oauth.html。

在您完成这些工作,您将会得到 OAuth Consumer Key 及其 OAuth Consumer Secret,用于我们下面的开发工作。

如何获得 OAuth Access Token

以下的代码是基于 Google Code 上提供的 OAuth Java 库进行开发的,读者可以从 http://oauth.googlecode.com/svn/code/java/core/ 下载获得。

  • 指定 Request Token URL,User Authorization URL,以及 Access Token URL,构造 OAuthServiceProvider 对象:
    OAuthServiceProvider serviceProvider = new OAuthServiceProvider( 
        "https://www.google.com/accounts/OAuthGetRequestToken", 
        "https://www.google.com/accounts/OAuthAuthorizeToken", 
        "https://www.google.com/accounts/OAuthGetAccessToken");
  • 指定 Customer Key,Customer Secret 以及 OAuthServiceProvider,构造 OAuthConsumer 对象:
    OAuthConsumer oauthConsumer = new OAuthConsumer(null 
        , "www.example.com" 
        , "hIsGkM+T4+90fKNesTtJq8Gs"
        , serviceProvider);
  • 为 OAuthConsumer 指定签名方法,以及提供您自签名 X509 数字证书的 private key。
    oauthConsumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.RSA_SHA1);
    oauthConsumer.setProperty(RSA_SHA1.PRIVATE_KEY, privateKey);
  • 由 OAuthConsumer 对象生成相应的 OAuthAccessor 对象:
     accessor = new OAuthAccessor(consumer);
  • 指定您想要访问的 Google 服务,在这里我们使用的是 Calendar 服务:
     Collection<? extends Map.Entry> parameters 
        = OAuth.newList("scope","http://www.google.com/calendar/feeds/");
  • 通过 OAuthClient 获得 Request Token:
    OAuthMessage response = getOAuthClient().getRequestTokenResponse( 
        accessor, null, parameters);

    使用 Request Token, 将用户重定向到授权页面,如图 7 所示:

  • 图 7. OAuth User Authorization

    图 7. OAuth User Authorization

  • 当用户点击“Grant access”按钮,完成授权后,再次通过 OAuthClient 获得 Access Token:
    oauthClient.getAccessToken(accessor, null, null);

在上述步骤成功完成后,Access Token 将保存在 accessor 对象的 accessToken 成员变量里。查看您的 Google Account 安全管理页面,可以看到您授权的所有消费方,如图 8 所示。

图 8. Authorized OAuth Access to your Google Account

图 8. Authorized OAuth Access to your Google Account

使用 OAuth Access Token 访问 Google 服务

接下来,我们使用上一节获得的 Access Token 设置 Google Service 的 OAuth 认证参数,然后从 Google Service 获取该用户的 Calendar 信息:

OAuthParameters para = new OAuthParameters(); 
para.setOAuthConsumerKey("www.example.com"); 
para.setOAuthToken(accessToken); 
googleService.setOAuthCredentials(para, signer);

清单 1 是完整的示例代码,供读者参考。

清单 1. 基于 OAuth 认证的 Google Service 消费方实现
import java.util.Collection; 
import java.util.Map; 
import net.oauth.OAuth; 
import net.oauth.OAuthAccessor; 
import net.oauth.OAuthConsumer; 
import net.oauth.client.OAuthClient; 

 public class DesktopClient { 
    private final OAuthAccessor accessor; 
    private OAuthClient oauthClient = null; 
    public DesktopClient(OAuthConsumer consumer) { 
        accessor = new OAuthAccessor(consumer); 
    } 

    public OAuthClient getOAuthClient() { 
        return oauthClient; 
    } 

    public void setOAuthClient(OAuthClient client) { 
        this.oauthClient = client; 
    } 

    //get the OAuth access token. 
    public String getAccessToken(String httpMethod, 	
	    Collection<? extends Map.Entry> parameters) throws Exception { 
        getOAuthClient().getRequestTokenResponse(accessor, null,parameters); 

        String authorizationURL = OAuth.addParameters( 
		    accessor.consumer.serviceProvider.userAuthorizationURL, 
			 OAuth.OAUTH_TOKEN, accessor.requestToken); 

        //Launch the browser and redirects user to authorization URL 
        Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " 
		    + authorizationURL); 

        //wait for user's authorization 
        System.out.println("Please authorize your OAuth request token. " 
		    + "Once that is complete, press any key to continue..."); 
        System.in.read(); 
        oauthClient.getAccessToken(accessor, null, null); 
        return accessor.accessToken; 
    } 
 } 

 import java.net.URL; 
 import java.security.KeyFactory; 
 import java.security.PrivateKey; 
 import java.security.spec.EncodedKeySpec; 
 import java.security.spec.PKCS8EncodedKeySpec; 
 import java.util.Collection; 
 import java.util.Map; 
 import com.google.gdata.client.GoogleService; 
 import com.google.gdata.client.authn.oauth.OAuthParameters; 
 import com.google.gdata.client.authn.oauth.OAuthRsaSha1Signer; 
 import com.google.gdata.client.authn.oauth.OAuthSigner; 
 import com.google.gdata.data.BaseEntry; 
 import com.google.gdata.data.BaseFeed; 
 import com.google.gdata.data.Feed; 
 import net.oauth.OAuth; 
 import net.oauth.OAuthConsumer; 
 import net.oauth.OAuthMessage; 
 import net.oauth.OAuthServiceProvider; 
 import net.oauth.client.OAuthClient; 
 import net.oauth.client.httpclient4.HttpClient4; 
 import net.oauth.example.desktop.MyGoogleService; 
 import net.oauth.signature.OAuthSignatureMethod; 
 import net.oauth.signature.RSA_SHA1; 

 public class GoogleOAuthExample { 
    //Note, use the private key of your self-signed X509 certificate. 
    private static final String PRIVATE_KEY = "XXXXXXXX"; 

    public static void main(String[] args) throws Exception { 
        KeyFactory fac = KeyFactory.getInstance("RSA"); 
        //PRIVATE_KEY is the private key of your self-signed X509 certificate. 
        EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec( 
		    OAuthSignatureMethod.decodeBase64(PRIVATE_KEY)); 
        fac = KeyFactory.getInstance("RSA"); 
        PrivateKey privateKey = fac.generatePrivate(privKeySpec); 
        OAuthServiceProvider serviceProvider = new OAuthServiceProvider( 
            //used for obtaining a request token 
			 //"https://www.google.com/accounts/OAuthGetRequestToken", 
	        //used for authorizing the request token 
            "https://www.google.com/accounts/OAuthAuthorizeToken", 
             //used for upgrading to an access token 
            "https://www.google.com/accounts/OAuthGetAccessToken"); 

        OAuthConsumer oauthConsumer = new OAuthConsumer(null 
            , "lszhy.weebly.com" //consumer key 
            , "hIsGnM+T4+86fKNesUtJq7Gs" //consumer secret 
            , serviceProvider); 

        oauthConsumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.RSA_SHA1); 
        oauthConsumer.setProperty(RSA_SHA1.PRIVATE_KEY, privateKey); 

        DesktopClient client = new DesktopClient(oauthConsumer); 
        client.setOAuthClient(new OAuthClient(new HttpClient4())); 
		
        Collection<? extends Map.Entry> parameters = 
		    OAuth.newList("scope","http://www.google.com/calendar/feeds/");
		
        String accessToken = client.getAccessToken(OAuthMessage.GET,parameters); 
		
		
        //Make an OAuth authorized request to Google 
		
        // Initialize the variables needed to make the request 
        URL feedUrl = new URL( 
		    "http://www.google.com/calendar/feeds/default/allcalendars/full");
        
        System.out.println("Sending request to " + feedUrl.toString()); 
        System.out.println(); 
        
        GoogleService googleService = new GoogleService("cl", "oauth-sample-app"); 

        OAuthSigner signer = new OAuthRsaSha1Signer(MyGoogleService.PRIVATE_KEY); 
        
        // Set the OAuth credentials which were obtained from the step above. 
        OAuthParameters para = new OAuthParameters(); 
        para.setOAuthConsumerKey("lszhy.weebly.com"); 
        para.setOAuthToken(accessToken); 
        googleService.setOAuthCredentials(para, signer); 
        
        // Make the request to Google 
        BaseFeed resultFeed = googleService.getFeed(feedUrl, Feed.class); 
        System.out.println("Response Data:");               
        System.out.println("=========================================="); 

        System.out.println("|TITLE: " + resultFeed.getTitle().getPlainText()); 
        if (resultFeed.getEntries().size() == 0) { 
           System.out.println("|\tNo entries found."); 
        } else { 
            for (int i = 0; i < resultFeed.getEntries().size(); i++) { 
               BaseEntry entry = (BaseEntry) resultFeed.getEntries().get(i); 
               System.out.println("|\t" + (i + 1) + ": "
                    + entry.getTitle().getPlainText()); 
            } 
        } 
        System.out.println("=========================================="); 	
    } 
 }

小结

OAuth 协议作为一种开放的,基于用户登录的授权认证方式,目前互联网很多 Open API 都对 OAuth 提供了支持,这包括 Google, Yahoo,Twitter 等。本文以 Google 为例子,介绍了 Java 桌面程序如何开发 OAuth 认证应用。在开发桌面应用访问 Web 资源这样一类程序时,一般通行的步骤是:使用 OAuth 做认证,然后使用获得的 OAuth Access Token,通过 REST API 访问用户在服务提供方的资源。

事实上,目前 OAuth 正通过许多实现(包括针对 Java、C#、Objective-C、Perl、PHP 及 Ruby 语言的实现)获得巨大的动力。大部分实现都由 OAuth 项目维护并放在 Google 代码库 (http://oauth.googlecode.com/svn/) 上。开发者可以利用这些 OAuth 类库编写自己需要的 OAuth 应用。

参考资料

Eclipse智能提示及快捷键

1、java智能提示

(1). 打开Eclipse,选择打开” Window - Preferences”。

(2). 在目录树上选择”Java-Editor-Content Assist”,在右侧的”Auto-Activation”找到”Auto Activation triggers for java”选项。默认触发代码提示的就是”.”这个符号。

(3). 在”Auto Activation triggers for java”选项中,将”.”更改:.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

2、XML智能提示

(1). 打开Eclipse,选择打开” Window - Preferences”。

(2). 在目录树上选择”XML-Editor-Content Assist”,在右侧的”Auto-Activation”找到”Prompt when these characters are inserted “选项。

(3). 在”Prompt when these characters are inserted”选项中,将”<=: ,”更改:<=:.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVW(,

3、快捷键

(1)Ctrl+Space

说明:内容助理。提供对方法,变量,参数,javadoc等得提示,应运在多种场合,总之需要提示的时候可先按此快捷键。注:避免输入法的切换设置与此设置冲突

(2)Ctrl+Shift+Space

说明:变量提示

(3)Ctrl+/

说明:添加/消除//注释,在eclipse2.0中,消除注释为Ctrl+\

(4)Ctrl+Shift+/

说明:添加/* */注释

(5)Ctrl+Shift+\

说明:消除/* */注释

(6)Ctrl+Shift+F

说明:自动格式化代码

(7)Ctrl+1

说明:批量修改源代码中的变量名,此外还可用在catch块上.

(8)Ctril+F6

说明:界面切换

(9)Ctril+Shift+M

说明:查找所需要得包

(10)Ctril+Shift+O

说明:自动引入所需要得包

(11)Ctrl+Alt+S

说明:源代码得快捷菜单。其中的Generate getters and setters 和 Surround with try/catchblock比较常用.建议把它们添加为快捷键.快捷键设置在 windows->preferences->Workbench->Keys

4、跟踪调式

单步返回 F7

单步跳过 F6

单步跳入 F5

单步跳入选择 Ctrl+F5

调试上次启动 F11

继续 F8

使用过滤器单步执行 Shift+F5

添加/去除断点 Ctrl+Shift+B

显示 Ctrl+D

运行上次启动 Ctrl+F11

运行至行 Ctrl+R

Ctrl+U重构作用域 功能 快捷键

撤销重构 Alt+Shift+Z

抽取方法 Alt+Shift+M

抽取局部变量 Alt+Shift+L

内联 Alt+Shift+I

移动 Alt+Shift+V

重命名 Alt+Shift+R

重做 Alt+Shift+Y

Android中简单使用Sqlite

当数据比较复杂的时候需要保存数据到数据库中,那么Android中默认集成了sqlite数据库,这是一个被广泛使用的轻量数据库,在很多场合都使用到,Android中提供了很多便利的api来使用,下面来看看入门级别的几个api.

1.打开和关闭数据库

//打开或创建test.db数据库  
SQLiteDatabase db = openOrCreateDatabase("test.db", Context.MODE_PRIVATE, null); 

//...对数据的操作

//关闭当前数据库  
db.close();  
//删除test.db数据库  
deleteDatabase("test.db");

2.使用sql语句进行操作数据,使用db.execSQL(…)这个方法执行写好的sql

//删除表
db.execSQL("DROP TABLE IF EXISTS person");  
//创建person表  
db.execSQL("CREATE TABLE person (id INTEGER, name VARCHAR, age SMALLINT)");  
//插入数据  
db.execSQL("INSERT INTO person VALUES (?, ?, ?)", new Object[]{1,"fullstacks.cn",28}); 
//查询数据
Cursor c = db.rawQuery("SELECT * FROM person WHERE age >= ?", new String[]{"20"});  
while (c.moveToNext()) {  
	int id = c.getInt(c.getColumnIndex("id"));  
	String name = c.getString(c.getColumnIndex("name"));  
	int age = c.getInt(c.getColumnIndex("age"));  
	Log.i("db", "id=>" + id + ", name=>" + name + ", age=>" + age);  
}
//删除数据
db.execSQL("DELETE FROM person WHERE id = ?", new String[]{1});
//更新数据
db.execSQL("UPDATE person set age = ? WHERE id = ?", new String[]{29,1});

3.使用android提供的api进行增删改

//新增
//ContentValues以键值对的形式存放数据  
ContentValues cv = new ContentValues();  
cv.put("name", "www.fullstacks.cn");  
cv.put("age", 30);  
cv.put("id", 2); 
//插入ContentValues中的数据  
db.insert("person", null, cv); 

//删除
db.delete("person", "age < ?", new String[]{"30"}); 

//更新数据
cv = new ContentValues();  
cv.put("age", 35);  //这是更新的内容
db.update("person", cv, "name = ?", new String[]{"john"}); //后面的两个参数是更新的条件

//查询数据还是使用sql查询 db.rawQuery

数据库创建之后文件存放在 /data/data/[PACKAGE_NAME]/databases目录下,可以使用文件浏览器查看

以上的几个简单api对基本的数据操作就已经满足了,更多的操作比如对数据库的升级维护之类的,更多的游标操作等可以查看api.

总结

本质上sqlite数据库在移动应用中的使用和服务端的使用没什么区别,也可以做一些实体和数据之间的映射,在移动应用中这些复杂的数据访问层并不是很需要,所以很多数据访问层的服务端框架并没有在Android上实现,使用这些基本的api都已经全部搞定,最多就是做一些简单的封装。