让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"

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



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服务器更新应用,另外一台照样提供服务,更新完一台服务器之后再更新另外一台服务器,整个系统看上去是没有停止的,这样就可以做到不间断服务的效果。

linux下安装redis

从官网下载最新版本的redis,http://redis.io/download

安装

1.解压

tar zxf redis-3.0.6.tar.gz

2.进入加压目录,安装

cd redis-3.0.6
make PREFIX=/usr/local/redis install #安装到指定目录中

需要将配置文件从原文件目录复制到目标路径

cp ./redis.conf /etc/redis/6379.conf

配置文件以端口命名,以便可以安装多个redis实例,启动脚本容易区分

修改配置文件里面的配置其中的一行:daemonize yes ,默认是no不在后台运行,想要作为linux服务在开机的时候启动,这里必须配置为yes.

3.配置系统启动服务

cp ./utils/redis_init_script /etc/rc.d/init.d/redis
chkconfig --add redis

添加服务的时候会报错,先把启动脚本修改一下

修改内容如下:

#!/bin/sh 
#chkconfig: 2345 80 90 
# Simple Redis init.d script conceived to work on Linux systems 
# as it does use of the /proc filesystem. 
   
REDISPORT=6379 
EXEC=/usr/local/redis/bin/redis-server 
CLIEXEC=/usr/local/redis/bin/redis-cli 
   
PIDFILE=/var/run/redis_${REDISPORT}.pid 
CONF="/etc/redis/${REDISPORT}.conf"

注意这个脚本里面启动命令后面要加上&以便系统在后台执行

$EXEC $CONF &

4.配置自动启动

chkconfig redis on

客户端

查看数据的客户端,可以看看国人开发的redisclient

下载地址:https://github.com/caoxinyu/RedisClient

客户端是java开发,运行命令

java -jar redisclient-win32.x86_64.2.0.jar

查看数据效果如下:

image

开发库

redis的开发库支持绝大多数语言,官方列表:http://redis.io/clients

java有很多,用的相对广泛的依赖库比如jedis,maven依赖如下:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.7.2</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>

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不在自动重启了,页面访问正常并执行了修改之后的代码,断点在新的代码上也没问题。

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 应用。

参考资料

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都已经全部搞定,最多就是做一些简单的封装。

Android给ListView添加搜索功能

当列表的数据比较多的时候,给列表添加搜索功能是非常有必要的,根据搜索框输入的内容过滤列表的信息,很多应用都这样的功能,只是各种实现方式不一样而已,下面来看看最简单的一个实现方式。

1.创建主界面

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
     
    <!-- Editext for Search -->
    <EditText android:id="@+id/inputSearch"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:hint="Search products.."
        android:inputType="textVisiblePassword"/>
  
    <!-- List View -->
    <ListView
        android:id="@+id/list_view"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />
  
</LinearLayout>

2.列表行布局文件

list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
     
    <!-- Single ListItem -->
     
    <!-- Product Name -->
    <TextView android:id="@+id/product_name"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:padding="10dip"
        android:textSize="16dip"
        android:textStyle="bold"/>    
 
</LinearLayout>

3.MainActivity.java的代码实现

public class MainActivity extends Activity {
     
    // List view
    private ListView lv;
     
    // Listview Adapter
    ArrayAdapter<String> adapter;
     
    // Search EditText
    EditText inputSearch;
     
     
    // ArrayList for Listview
    ArrayList<HashMap<String, String>> productList;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
         
        // Listview Data
        String products[] = {"Dell Inspiron", "HTC One X", "HTC Wildfire S", "HTC Sense", "HTC Sensation XE",
                                "iPhone 4S", "Samsung Galaxy Note 800",
                                "Samsung Galaxy S3", "MacBook Air", "Mac Mini", "MacBook Pro"};
         
        lv = (ListView) findViewById(R.id.list_view);
        inputSearch = (EditText) findViewById(R.id.inputSearch);
         
        // Adding items to listview
        adapter = new ArrayAdapter<String>(this, R.layout.list_item, R.id.product_name, products);
        lv.setAdapter(adapter);       
         
    }
     
}

运行之后的效果图

image

4.给搜索框添加事件

MainActivity.java
inputSearch.addTextChangedListener(new TextWatcher() {
     
    @Override
    public void onTextChanged(CharSequence cs, int arg1, int arg2, int arg3) {
        // When user changed the Text
        MainActivity.this.adapter.getFilter().filter(cs);   
    }
     
    @Override
    public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
            int arg3) {
        // TODO Auto-generated method stub
         
    }
     
    @Override
    public void afterTextChanged(Editable arg0) {
        // TODO Auto-generated method stub                          
    }
});

这样所有的事情就做完了

总结

这个例子只是实现了简单的对当前显示的列表进行过滤,而且是实时的根据输入字母进行过滤,那么很多时候真正的应用是需去后台进行搜索,由于网速和查询速度的限制,对输入进行实时搜索可能不太现实,那么可以加一个搜索按钮,用户输入完毕之后再点击搜索按钮,然后再从后台获取数据进行刷新列表。

Android中的XML动画

在App中加入一些动画效果会让界面更加丰富多彩,动画可以通过XML定义来实现,当然也可以通过代码来实现, 动画效果一般使用在界面切换之间的效果,或者加载一些界面的效果,比如淡入淡出之类的。

通过XML创建一个动画是很简单的,只需要简单的xml和简单一些代码就可以完成

1.创建XML定义文件,位于res->anim->animation.xml文件中,如果不存在文件夹anim可以创建一个

image

2.fade_in.xml淡入效果

fade_in.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true" >
 
    <alpha
        android:duration="1000"
        android:fromAlpha="0.0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toAlpha="1.0" />
 
</set>

3.加载动画

FadeInActivity.java
public class FadeInActivity extends Activity{
 
    TextView txtMessage;
 
    // Animation
    Animation animFadein;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fadein);
 
        txtMessage = (TextView) findViewById(R.id.txtMessage);
 
        // load the animation
        animFadein = AnimationUtils.loadAnimation(getApplicationContext(),
                R.anim.fade_in);        
    }
}

4.可以监听动画的事件

public class FadeInActivity extends Activity implements AnimationListener {
.
.
.
// set animation listener
animFadein.setAnimationListener(this);
.
.
.
// animation listeners
    @Override
    public void onAnimationEnd(Animation animation) {
        // Take any action after completing the animation
        // check for fade in animation
        if (animation == animFadein) {
            Toast.makeText(getApplicationContext(), "Animation Stopped",
                    Toast.LENGTH_SHORT).show();
        }
 
    }
 
    @Override
    public void onAnimationRepeat(Animation animation) {
        // Animation is repeating
    }
 
    @Override
    public void onAnimationStart(Animation animation) {
        // Animation started
    }

5.最后在某个界面元素上启动动画

// start the animation
txtMessage.startAnimation(animFadein);

以下是完整的代码

public class FadeInActivity extends Activity implements AnimationListener {
 
    TextView txtMessage;
    Button btnStart;
 
    // Animation
    Animation animFadein;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fadein);
 
        txtMessage = (TextView) findViewById(R.id.txtMessage);
        btnStart = (Button) findViewById(R.id.btnStart);
 
        // load the animation
        animFadein = AnimationUtils.loadAnimation(getApplicationContext(),
                R.anim.fade_in);
         
        // set animation listener
        animFadein.setAnimationListener(this);
 
        // button click event
        btnStart.setOnClickListener(new View.OnClickListener() {
 
            @Override
            public void onClick(View v) {
                txtMessage.setVisibility(View.VISIBLE);
                 
                // start the animation
                txtMessage.startAnimation(animFadein);
            }
        });
 
    }
 
    @Override
    public void onAnimationEnd(Animation animation) {
        // Take any action after completing the animation
 
        // check for fade in animation
        if (animation == animFadein) {
            Toast.makeText(getApplicationContext(), "Animation Stopped",
                    Toast.LENGTH_SHORT).show();
        }
 
    }
 
    @Override
    public void onAnimationRepeat(Animation animation) {
        // TODO Auto-generated method stub
 
    }
 
    @Override
    public void onAnimationStart(Animation animation) {
        // TODO Auto-generated method stub
 
    }
 
}

以下是常用的一些动画

1. Fade In
2. Fade Out
3. Cross Fading
4. Blink
5. Zoom In
6. Zoom Out
7. Rotate
8. Move
9. Slide Up
10. Slide Down
11. Bounce
12. Sequential Animation
13. Together Animation

fade_in.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true" >
 
    <alpha
        android:duration="1000"
        android:fromAlpha="0.0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toAlpha="1.0" />
 
</set>
fade_out.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true" >
 
    <alpha
        android:duration="1000"
        android:fromAlpha="1.0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toAlpha="0.0" />
 
</set>
//Cross Fading
TextView txtView1, txtView2;
Animation animFadeIn, animFadeOut;
.
.
// load animations
animFadeIn = AnimationUtils.loadAnimation(getApplicationContext(),
                R.anim.fade_in);
animFadeOut = AnimationUtils.loadAnimation(getApplicationContext(),
                R.anim.fade_out);
.
.
// set animation listeners
animFadeIn.setAnimationListener(this);
animFadeOut.setAnimationListener(this);
 
.
.
// Make fade in elements Visible first
txtMessage2.setVisibility(View.VISIBLE);
 
// start fade in animation
txtMessage2.startAnimation(animFadeIn);
                 
// start fade out animation
txtMessage1.startAnimation(animFadeOut);
blink.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha android:fromAlpha="0.0"
        android:toAlpha="1.0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:duration="600"
        android:repeatMode="reverse"
        android:repeatCount="infinite"/>
</set>
zoom_in.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true" >
 
    <scale
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="1000"
        android:fromXScale="1"
        android:fromYScale="1"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toXScale="3"
        android:toYScale="3" >
    </scale>
 
</set>
zoom_out.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true" >
 
    <scale
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="1000"
        android:fromXScale="1.0"
        android:fromYScale="1.0"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toXScale="0.5"
        android:toYScale="0.5" >
    </scale>
 
</set>
rotate.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <rotate android:fromDegrees="0"
        android:toDegrees="360"
        android:pivotX="50%"
        android:pivotY="50%"
        android:duration="600"
        android:repeatMode="restart"
        android:repeatCount="infinite"
        android:interpolator="@android:anim/cycle_interpolator"/>
 
</set>
move.xml
<?xml version="1.0" encoding="utf-8"?>
<set
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/linear_interpolator"
    android:fillAfter="true">
 
   <translate
        android:fromXDelta="0%p"
        android:toXDelta="75%p"
        android:duration="800" />
</set>
slide_up.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true" >
 
    <scale
        android:duration="500"
        android:fromXScale="1.0"
        android:fromYScale="1.0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXScale="1.0"
        android:toYScale="0.0" />
 
</set>
slide_down.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true">
 
    <scale
        android:duration="500"
        android:fromXScale="1.0"
        android:fromYScale="0.0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXScale="1.0"
        android:toYScale="1.0" />
 
</set>
bounce.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true"
    android:interpolator="@android:anim/bounce_interpolator">
 
    <scale
        android:duration="500"
        android:fromXScale="1.0"
        android:fromYScale="0.0"
        android:toXScale="1.0"
        android:toYScale="1.0" />
 
</set>
sequential.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true"
    android:interpolator="@android:anim/linear_interpolator" >
 
    <!-- Use startOffset to give delay between animations -->
 
 
    <!-- Move -->
    <translate
        android:duration="800"
        android:fillAfter="true"
        android:fromXDelta="0%p"
        android:startOffset="300"
        android:toXDelta="75%p" />
    <translate
        android:duration="800"
        android:fillAfter="true"
        android:fromYDelta="0%p"
        android:startOffset="1100"
        android:toYDelta="70%p" />
    <translate
        android:duration="800"
        android:fillAfter="true"
        android:fromXDelta="0%p"
        android:startOffset="1900"
        android:toXDelta="-75%p" />
    <translate
        android:duration="800"
        android:fillAfter="true"
        android:fromYDelta="0%p"
        android:startOffset="2700"
        android:toYDelta="-70%p" />
 
    <!-- Rotate 360 degrees -->
    <rotate
        android:duration="1000"
        android:fromDegrees="0"
        android:interpolator="@android:anim/cycle_interpolator"
        android:pivotX="50%"
        android:pivotY="50%"
        android:startOffset="3800"
        android:repeatCount="infinite"
        android:repeatMode="restart"
        android:toDegrees="360" />
 
</set>
together.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true"
    android:interpolator="@android:anim/linear_interpolator" >
 
    <scale
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000"
        android:fromXScale="1"
        android:fromYScale="1"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toXScale="4"
        android:toYScale="4" >
    </scale>
 
    <!-- Rotate 180 degrees -->
    <rotate
        android:duration="500"
        android:fromDegrees="0"
        android:pivotX="50%"
        android:pivotY="50%"
        android:repeatCount="infinite"
        android:repeatMode="restart"
        android:toDegrees="360" />
 
</set>

总结

在App中适当的加入一下动画确实是可以达到画龙点睛的效果,但是胡乱的使用也是很容易搞的乱七八糟,追根揭底还是设计层面上的问题,好的创意往往都是可遇不可求。

Android Expandable List View

可展开的列表主要用于分组或者分类的列表,在列表中一组就是一行,展开一行可以显示这一组的子列表,也可以收起来节省空间,每一行即每一组可以看做两部分组成,就是头部和子列表部分,点击头部即可展开或者收起,这种组件的应用也很普遍。

1.创建主布局文件

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical"
    android:background="#f4f4f4" >
 
            <ExpandableListView
                android:id="@+id/lvExp"
                android:layout_height="match_parent"
                android:layout_width="match_parent"/>   
 
</LinearLayout>

2.创建组的布局文件即组的头部

list_group.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="8dp"
    android:background="#000000">
 
 
    <TextView
        android:id="@+id/lblListHeader"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:paddingLeft="?android:attr/expandableListPreferredItemPaddingLeft"
        android:textSize="17dp"
        android:textColor="#f9f93d" />
 
</LinearLayout>

3.创建子列表行布局文件

list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="55dip"
    android:orientation="vertical" >
 
    <TextView
        android:id="@+id/lblListItem"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:textSize="17dip"
        android:paddingTop="5dp"
        android:paddingBottom="5dp"
        android:paddingLeft="?android:attr/expandableListPreferredChildPaddingLeft" />
 
</LinearLayout>

4.创建Adapter继承自BaseExpandableAdapter

public class ExpandableListAdapter extends BaseExpandableListAdapter {
 
    private Context _context;
    private List<String> _listDataHeader; // header titles
    // child data in format of header title, child title
    private HashMap<String, List<String>> _listDataChild;
 
    public ExpandableListAdapter(Context context, List<String> listDataHeader,
            HashMap<String, List<String>> listChildData) {
        this._context = context;
        this._listDataHeader = listDataHeader;
        this._listDataChild = listChildData;
    }
 
    @Override
    public Object getChild(int groupPosition, int childPosititon) {
        return this._listDataChild.get(this._listDataHeader.get(groupPosition))
                .get(childPosititon);
    }
 
    @Override
    public long getChildId(int groupPosition, int childPosition) {
        return childPosition;
    }
 
    @Override
    public View getChildView(int groupPosition, final int childPosition,
            boolean isLastChild, View convertView, ViewGroup parent) {
 
        final String childText = (String) getChild(groupPosition, childPosition);
 
        if (convertView == null) {
            LayoutInflater infalInflater = (LayoutInflater) this._context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = infalInflater.inflate(R.layout.list_item, null);
        }
 
        TextView txtListChild = (TextView) convertView
                .findViewById(R.id.lblListItem);
 
        txtListChild.setText(childText);
        return convertView;
    }
 
    @Override
    public int getChildrenCount(int groupPosition) {
        return this._listDataChild.get(this._listDataHeader.get(groupPosition))
                .size();
    }
 
    @Override
    public Object getGroup(int groupPosition) {
        return this._listDataHeader.get(groupPosition);
    }
 
    @Override
    public int getGroupCount() {
        return this._listDataHeader.size();
    }
 
    @Override
    public long getGroupId(int groupPosition) {
        return groupPosition;
    }
 
    @Override
    public View getGroupView(int groupPosition, boolean isExpanded,
            View convertView, ViewGroup parent) {
        String headerTitle = (String) getGroup(groupPosition);
        if (convertView == null) {
            LayoutInflater infalInflater = (LayoutInflater) this._context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = infalInflater.inflate(R.layout.list_group, null);
        }
 
        TextView lblListHeader = (TextView) convertView
                .findViewById(R.id.lblListHeader);
        lblListHeader.setTypeface(null, Typeface.BOLD);
        lblListHeader.setText(headerTitle);
 
        return convertView;
    }
 
    @Override
    public boolean hasStableIds() {
        return false;
    }
 
    @Override
    public boolean isChildSelectable(int groupPosition, int childPosition) {
        return true;
    }
}

5.在Activity中使用

public class MainActivity extends Activity {
 
    ExpandableListAdapter listAdapter;
    ExpandableListView expListView;
    List<String> listDataHeader;
    HashMap<String, List<String>> listDataChild;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        // get the listview
        expListView = (ExpandableListView) findViewById(R.id.lvExp);
 
        // preparing list data
        prepareListData();
 
        listAdapter = new ExpandableListAdapter(this, listDataHeader, listDataChild);
 
        // setting list adapter
        expListView.setAdapter(listAdapter);
    }
 
    /*
     * Preparing the list data
     */
    private void prepareListData() {
        listDataHeader = new ArrayList<String>();
        listDataChild = new HashMap<String, List<String>>();
 
        // Adding child data
        listDataHeader.add("Top 250");
        listDataHeader.add("Now Showing");
        listDataHeader.add("Coming Soon..");
 
        // Adding child data
        List<String> top250 = new ArrayList<String>();
        top250.add("The Shawshank Redemption");
        top250.add("The Godfather");
        top250.add("The Godfather: Part II");
        top250.add("Pulp Fiction");
        top250.add("The Good, the Bad and the Ugly");
        top250.add("The Dark Knight");
        top250.add("12 Angry Men");
 
        List<String> nowShowing = new ArrayList<String>();
        nowShowing.add("The Conjuring");
        nowShowing.add("Despicable Me 2");
        nowShowing.add("Turbo");
        nowShowing.add("Grown Ups 2");
        nowShowing.add("Red 2");
        nowShowing.add("The Wolverine");
 
        List<String> comingSoon = new ArrayList<String>();
        comingSoon.add("2 Guns");
        comingSoon.add("The Smurfs 2");
        comingSoon.add("The Spectacular Now");
        comingSoon.add("The Canyons");
        comingSoon.add("Europa Report");
 
        listDataChild.put(listDataHeader.get(0), top250); // Header, Child data
        listDataChild.put(listDataHeader.get(1), nowShowing);
        listDataChild.put(listDataHeader.get(2), comingSoon);
    }
}

代码就这么多了,运行之后就看到效果了,如下图所示

image

监听列表展开和收起的事件和子节点的点击事件

// Listview Group expanded listener
expListView.setOnGroupExpandListener(new OnGroupExpandListener() {
 
    @Override
    public void onGroupExpand(int groupPosition) {
        Toast.makeText(getApplicationContext(),
                listDataHeader.get(groupPosition) + " Expanded",
                Toast.LENGTH_SHORT).show();
    }
});
// Listview Group collasped listener
expListView.setOnGroupCollapseListener(new OnGroupCollapseListener() {
 
    @Override
    public void onGroupCollapse(int groupPosition) {
        Toast.makeText(getApplicationContext(),
                listDataHeader.get(groupPosition) + " Collapsed",
                Toast.LENGTH_SHORT).show();
 
    }
});
// Listview on child click listener
expListView.setOnChildClickListener(new OnChildClickListener() {

	@Override
	public boolean onChildClick(ExpandableListView parent, View v,
			int groupPosition, int childPosition, long id) {
		Toast.makeText(
				getApplicationContext(),
				listDataHeader.get(groupPosition)
						+ " : "
						+ listDataChild.get(
								listDataHeader.get(groupPosition)).get(
								childPosition), Toast.LENGTH_SHORT)
				.show();
		return false;
	}
});