前言:

  由于项目的原因,需要对项目中大量访问多修改少的数据进行缓存并管理,为达到开发过程中通过Annotation简单的配置既可以完成对缓存的设置与更新的需求,故而设计的该简易的解决方案。

涉及技术:

1.Spring AOP

2.Java Annotation

3.Memcache (项目中使用的缓存组件)

4.JVM基础 (Class文件结构,用于解析出方法中的形参名称,动态生成缓存key,目测效率不高0.0)

5.Ognl (用于动态解析缓存的key)

实现细节:

Annotation:LoadFromMemcached 用于method之上的注解,作用是使带有该注解的method在调用的时候先经过缓存查询,缓存中查询不到再去数据库查询并将结果缓存至缓存服务器Memcache中,

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LoadFromMemcached {
String value();//缓存的key
int timeScope() default 600;//默认过期时间,单位秒
String condition() default "";//执行缓存查询的条件
}

 Annotation:UpdateForMemcached 类似于LoadFromMemcached,作用是使带有该注解的method在调用的时候更新缓存服务器中的缓存,

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface UpdateForMemcached {
String[] value();//可能有多个key需要更新
String condition() default "";//执行缓存的条件
}

  AOP:MemcachedCacheInterceptor 缓存AOP实现的核心类,用于对Annotation注解了的method进行拦截并进行相应的操作,

 import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Resource;
import net.rubyeye.xmemcached.MemcachedClient;
import net.rubyeye.xmemcached.exception.MemcachedException;
import ognl.Ognl;
import ognl.OgnlException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MemcachedCacheInterceptor {
private final String GET = "@annotation(LoadFromMemcached)";
private final String UPDATE = "@annotation(UpdateForMemcached)";
// 替换为其他缓存组件即可切换为其他缓存系统,这里是使用的Memcached。如果再抽象一层缓存系统管理,则可以动态的更换缓存系统。
@Resource
private MemcachedClient cache;
private Logger log = LoggerFactory
.getLogger(MemcachedCacheInterceptor.class);
/**
*
* @Title: get
* @Description: 首先从缓存中加载数据,缓存命中则返回数据,未命中则从数据库查找,并加入缓存
* @param @param call
* @param @return
* @param @throws Throwable
* @return Object
* @throws
*/
@Around(GET)
public Object get(ProceedingJoinPoint call) throws Throwable {
LoadFromMemcached anno = getAnnotation(call, LoadFromMemcached.class);
String key = anno.value();
int timeSocpe = anno.timeScope();
if (!executeCondition(anno.condition(), call)) {// 不满足条件,直接调用方法,不进行缓存AOP操作
return call.proceed();
}
key = getKeyNameFromParam(key, call);
Object value = null;
try {
value = cache.get(key);
} catch (TimeoutException e) {
log.error("Get Data From Memcached TimeOut!About Key:" + key, e);
e.printStackTrace();
} catch (InterruptedException e) {
log.error(
"Get Data From Memcached TimeOut And Interrupted!About Key:"
+ key, e);
e.printStackTrace();
} catch (MemcachedException e) {
log.error(
"Get Data From Memcached And Happend A Unexpected Error!About Key:"
+ key, e);
e.printStackTrace();
}
if (value == null) {
value = call.proceed();
if (value != null) {
try {
cache.add(key, timeSocpe, value);
log.info("Add Data For Memcached Success!About Key:" + key);
} catch (TimeoutException e) {
log.error(
"Add Data For Memcached TimeOut!About Key:" + key,
e);
e.printStackTrace();
} catch (InterruptedException e) {
log.error(
"Add Data For Memcached TimeOut And Interrupted!About Key:"
+ key, e);
e.printStackTrace();
} catch (MemcachedException e) {
log.error(
"Add Data For Memcached And Happend A Unexpected Error!About Key:"
+ key, e);
e.printStackTrace();
}
}
}
return value;
}
/**
*
* @Title: update
* @Description: 执行方法的同时更新缓存中的数据
* @param @param call
* @param @return
* @param @throws Throwable
* @return Object
* @throws
*/
@Around(UPDATE)
public Object update(ProceedingJoinPoint call) throws Throwable {
UpdateForMemcached anno = getAnnotation(call, UpdateForMemcached.class);
String[] key = anno.value();// 可能需要更新多个key
Object value = call.proceed();
if (!executeCondition(anno.condition(), call)) {// 不满足条件,直接调用方法,不进行缓存AOP操作
return value;
}
if (value != null) {
try {
for (String singleKey : key) {// 循环处理所有需要更新的key
String tempKey = getKeyNameFromParam(singleKey, call);
cache.delete(tempKey);
}
log.info("Update Data For Memcached Success!About Key:" + key);
} catch (TimeoutException e) {
log.error("Update Data For Memcached TimeOut!About Key:" + key,
e);
e.printStackTrace();
} catch (InterruptedException e) {
log.error(
"Update Data For Memcached TimeOut And Interrupted!About Key:"
+ key, e);
e.printStackTrace();
} catch (MemcachedException e) {
log.error(
"Update Data For Memcached And Happend A Unexpected Error!About Key:"
+ key, e);
e.printStackTrace();
}
}
return value;
}
/**
*
* @Title: getAnnotation
* @Description: 获得Annotation对象
* @param @param <T>
* @param @param jp
* @param @param clazz
* @param @return
* @return T
* @throws
*/
private <T extends Annotation> T getAnnotation(ProceedingJoinPoint jp,
Class<T> clazz) {
MethodSignature joinPointObject = (MethodSignature) jp.getSignature();
Method method = joinPointObject.getMethod();
return method.getAnnotation(clazz);
}
/**
*
* @Title: getKeyNameFromParam
* @Description: 获得组合后的KEY值
* @param @param key
* @param @param jp
* @param @return
* @return String
* @throws
*/
private String getKeyNameFromParam(String key, ProceedingJoinPoint jp) {
if (!key.contains("$")) {
return key;
}
String regexp = "\\$\\{[^\\}]+\\}";
Pattern pattern = Pattern.compile(regexp);
Matcher matcher = pattern.matcher(key);
List<String> names = new ArrayList<String>();
try {
while (matcher.find()) {
names.add(matcher.group());
}
key = executeNames(key, names, jp);
} catch (Exception e) {
log.error("Regex Parse Error!", e);
}
return key;
}
/**
*
* @Title: executeNames
* @Description: 对KEY中的参数进行替换
* @param @param key
* @param @param names
* @param @param jp
* @param @return
* @param @throws OgnlException
* @return String
* @throws
*/
private String executeNames(String key, List<String> names,
ProceedingJoinPoint jp) throws OgnlException {
Method method = ((MethodSignature) jp.getSignature()).getMethod();
// 形参列表
List<String> param = MethodParamNamesScaner.getParamNames(method);
if (names == null || names.size() == 0) {
return key;
}
Object[] params = jp.getArgs();
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0; i < param.size(); i++) {
map.put(param.get(i), params[i]);
}
for (String name : names) {
String temp = name.substring(2);
temp = temp.substring(0, temp.length() - 1);
key = myReplace(key, name, (String) Ognl.getValue(temp, map));
}
return key;
}
/**
*
* @Title: myReplace
* @Description: 不依赖Regex的替换,避免$符号、{}等在String.replaceAll方法中当做Regex处理时候的问题。
* @param @param src
* @param @param from
* @param @param to
* @param @return
* @return String
* @throws
*/
private String myReplace(String src, String from, String to) {
int index = src.indexOf(from);
if (index == -1) {
return src;
}
return src.substring(0, index) + to
+ src.substring(index + from.length());
}
/**
*
* @Title: executeCondition
* @Description: 判断是否需要进行缓存操作
* @param @param condition parm
* @param @return
* @return boolean true:需要 false:不需要
* @throws
*/
private boolean executeCondition(String condition, ProceedingJoinPoint jp) {
if ("".equals(condition)) {
return true;
}
Method method = ((MethodSignature) jp.getSignature()).getMethod();
// 形参列表
List<String> param = MethodParamNamesScaner.getParamNames(method);
if (param == null || param.size() == 0) {
return true;
}
Object[] params = jp.getArgs();
Map<String, Object> map = new HashMap<String, Object>();
for (int i = 0; i < param.size(); i++) {
map.put(param.get(i), params[i]);
}
boolean returnVal = false;
try {
returnVal = (Boolean) Ognl.getValue(condition, map);
} catch (OgnlException e) {
e.printStackTrace();
}
return returnVal;
}
public void setCache(MemcachedClient cache) {
this.cache = cache;
}
}
辅助类:借用MethodParamNamesScaner类与Ognl结合完成对缓存key的动态解析功能,  
 //引用至:https://gist.github.com/wendal/2011728,用于解析方法的形参名称
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 通过读取Class文件,获得方法形参名称列表
*
* @author wendal(wendal1985@gmail.com)
*
*/
public class MethodParamNamesScaner {
/**
* 获取Method的形参名称列表
*
* @param method
* 需要解析的方法
* @return 形参名称列表,如果没有调试信息,将返回null
*/
public static List<String> getParamNames(Method method) {
try {
int size = method.getParameterTypes().length;
if (size == 0)
return new ArrayList<String>(0);
List<String> list = getParamNames(method.getDeclaringClass()).get(
getKey(method));
if (list != null && list.size() != size)
return list.subList(0, size);
return list;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
/**
* 获取Constructor的形参名称列表
*
* @param constructor
* 需要解析的构造函数
* @return 形参名称列表,如果没有调试信息,将返回null
*/
public static List<String> getParamNames(Constructor<?> constructor) {
try {
int size = constructor.getParameterTypes().length;
if (size == 0)
return new ArrayList<String>(0);
List<String> list = getParamNames(constructor.getDeclaringClass())
.get(getKey(constructor));
if (list != null && list.size() != size)
return list.subList(0, size);
return list;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
// ---------------------------------------------------------------------------------------------------
/**
* 获取一个类的所有方法/构造方法的形参名称Map
*
* @param klass
* 需要解析的类
* @return 所有方法/构造方法的形参名称Map
* @throws IOException
* 如果有任何IO异常,不应该有,如果是本地文件,那100%遇到bug了
*/
public static Map<String, List<String>> getParamNames(Class<?> klass)
throws IOException {
InputStream in = klass.getResourceAsStream("/"
+ klass.getName().replace('.', '/') + ".class");
return getParamNames(in);
}
public static Map<String, List<String>> getParamNames(InputStream in)
throws IOException {
DataInputStream dis = new DataInputStream(new BufferedInputStream(in));
Map<String, List<String>> names = new HashMap<String, List<String>>();
Map<Integer, String> strs = new HashMap<Integer, String>();
dis.skipBytes(4);// Magic
dis.skipBytes(2);// 副版本号
dis.skipBytes(2);// 主版本号
// 读取常量池
int constant_pool_count = dis.readUnsignedShort();
for (int i = 0; i < (constant_pool_count - 1); i++) {
byte flag = dis.readByte();
switch (flag) {
case 7:// CONSTANT_Class:
dis.skipBytes(2);
break;
case 9:// CONSTANT_Fieldref:
case 10:// CONSTANT_Methodref:
case 11:// CONSTANT_InterfaceMethodref:
dis.skipBytes(2);
dis.skipBytes(2);
break;
case 8:// CONSTANT_String:
dis.skipBytes(2);
break;
case 3:// CONSTANT_Integer:
case 4:// CONSTANT_Float:
dis.skipBytes(4);
break;
case 5:// CONSTANT_Long:
case 6:// CONSTANT_Double:
dis.skipBytes(8);
i++;// 必须跳过一个,这是class文件设计的一个缺陷,历史遗留问题
break;
case 12:// CONSTANT_NameAndType:
dis.skipBytes(2);
dis.skipBytes(2);
break;
case 1:// CONSTANT_Utf8:
int len = dis.readUnsignedShort();
byte[] data = new byte[len];
dis.read(data);
strs.put(i + 1, new String(data, "UTF-8"));// 必然是UTF8的
break;
case 15:// CONSTANT_MethodHandle:
dis.skipBytes(1);
dis.skipBytes(2);
break;
case 16:// CONSTANT_MethodType:
dis.skipBytes(2);
break;
case 18:// CONSTANT_InvokeDynamic:
dis.skipBytes(2);
dis.skipBytes(2);
break;
default:
throw new RuntimeException("Impossible!! flag=" + flag);
}
}
dis.skipBytes(2);// 版本控制符
dis.skipBytes(2);// 类名
dis.skipBytes(2);// 超类
// 跳过接口定义
int interfaces_count = dis.readUnsignedShort();
dis.skipBytes(2 * interfaces_count);// 每个接口数据,是2个字节
// 跳过字段定义
int fields_count = dis.readUnsignedShort();
for (int i = 0; i < fields_count; i++) {
dis.skipBytes(2);
dis.skipBytes(2);
dis.skipBytes(2);
int attributes_count = dis.readUnsignedShort();
for (int j = 0; j < attributes_count; j++) {
dis.skipBytes(2);// 跳过访问控制符
int attribute_length = dis.readInt();
dis.skipBytes(attribute_length);
}
}
// 开始读取方法
int methods_count = dis.readUnsignedShort();
for (int i = 0; i < methods_count; i++) {
dis.skipBytes(2); // 跳过访问控制符
String methodName = strs.get(dis.readUnsignedShort());
String descriptor = strs.get(dis.readUnsignedShort());
short attributes_count = dis.readShort();
for (int j = 0; j < attributes_count; j++) {
String attrName = strs.get(dis.readUnsignedShort());
int attribute_length = dis.readInt();
if ("Code".equals(attrName)) { // 形参只在Code属性中
dis.skipBytes(2);
dis.skipBytes(2);
int code_len = dis.readInt();
dis.skipBytes(code_len); // 跳过具体代码
int exception_table_length = dis.readUnsignedShort();
dis.skipBytes(8 * exception_table_length); // 跳过异常表
int code_attributes_count = dis.readUnsignedShort();
for (int k = 0; k < code_attributes_count; k++) {
int str_index = dis.readUnsignedShort();
String codeAttrName = strs.get(str_index);
int code_attribute_length = dis.readInt();
if ("LocalVariableTable".equals(codeAttrName)) {// 形参在LocalVariableTable属性中
int local_variable_table_length = dis
.readUnsignedShort();
List<String> varNames = new ArrayList<String>(
local_variable_table_length);
for (int l = 0; l < local_variable_table_length; l++) {
dis.skipBytes(2);
dis.skipBytes(2);
String varName = strs.get(dis
.readUnsignedShort());
dis.skipBytes(2);
dis.skipBytes(2);
if (!"this".equals(varName)) // 非静态方法,第一个参数是this
varNames.add(varName);
}
names.put(methodName + "," + descriptor, varNames);
} else
dis.skipBytes(code_attribute_length);
}
} else
dis.skipBytes(attribute_length);
}
}
dis.close();
return names;
}
/**
* 传入Method或Constructor,获取getParamNames方法返回的Map所对应的key
*/
public static String getKey(Object obj) {
StringBuilder sb = new StringBuilder();
if (obj instanceof Method) {
sb.append(((Method) obj).getName()).append(',');
getDescriptor(sb, (Method) obj);
} else if (obj instanceof Constructor) {
sb.append("<init>,"); // 只有非静态构造方法才能用有方法参数的,而且通过反射API拿不到静态构造方法
getDescriptor(sb, (Constructor<?>) obj);
} else
throw new RuntimeException("Not Method or Constructor!");
return sb.toString();
}
public static void getDescriptor(StringBuilder sb, Method method) {
sb.append('(');
for (Class<?> klass : method.getParameterTypes())
getDescriptor(sb, klass);
sb.append(')');
getDescriptor(sb, method.getReturnType());
}
public static void getDescriptor(StringBuilder sb,
Constructor<?> constructor) {
sb.append('(');
for (Class<?> klass : constructor.getParameterTypes())
getDescriptor(sb, klass);
sb.append(')');
sb.append('V');
}
/** 本方法来源于ow2的asm库的Type类 */
public static void getDescriptor(final StringBuilder buf, final Class<?> c) {
Class<?> d = c;
while (true) {
if (d.isPrimitive()) {
char car;
if (d == Integer.TYPE) {
car = 'I';
} else if (d == Void.TYPE) {
car = 'V';
} else if (d == Boolean.TYPE) {
car = 'Z';
} else if (d == Byte.TYPE) {
car = 'B';
} else if (d == Character.TYPE) {
car = 'C';
} else if (d == Short.TYPE) {
car = 'S';
} else if (d == Double.TYPE) {
car = 'D';
} else if (d == Float.TYPE) {
car = 'F';
} else /* if (d == Long.TYPE) */{
car = 'J';
}
buf.append(car);
return;
} else if (d.isArray()) {
buf.append('[');
d = d.getComponentType();
} else {
buf.append('L');
String name = d.getName();
int len = name.length();
for (int i = 0; i < len; ++i) {
char car = name.charAt(i);
buf.append(car == '.' ? '/' : car);
}
buf.append(';');
return;
}
}
}
}
使用案例: 

1.使用缓存:

 /*
* value:缓存中的键,${map.name}会动态替换为传入参数map里面的key为name的值。
* comdition:缓存执行条件:!map.containsKey('execute')表示map中不包含execute这个key的时候才进行缓存操作。
* 这里面的map是传入的参数名称。
* 执行到该方法会自动去缓存里面查找该key,有就直接返回,没有就执行该方法,如果返回值不为空则同时存入缓存并返回结果。
*/
@LoadFromMemcached(value="Resource_selectByMap_${map.name}",condition="!map.containsKey('execute')" )
public List<Resource> selectByMap(Object map) {
return super.selectByMap(map);
}

表示执行该method(selectByMap)的时候会首先去缓存组件中查找数据,如果查找到数据就直接返回,如果找不到数据就执行方法体,并将返回值记录入缓存中。

2.更新缓存:

 /*
* 同样value为缓存中的key,${t.name}会动态替换为update方法传入参数Resource的name字段
* comdition:字段作用同上,不演示了
*/
@UpdateForMemcached(value="Resource_selectByMap_${t.name}")
public int update(Resource t) {
return super.update(t);
}

表示执行该method(update)的时候会同步将缓存中的key置为过期(并不是把该方法的返回值放入缓存,只是将对应的缓存设为过期,下次再执行selectByMap的时候获取的就是最新的数据了)。

扩展:

本文只是简单的解决方案,可能有很多不足的地方,欢迎交流,以此简单的结构为基础进行扩展,将MemcachedClient以及相关的缓存操作方法提取出来并完善细节即可完成基本通用的缓存组件。

 

基于Annotation与SpringAOP的缓存简单解决方案的更多相关文章

  1. [项目回顾]基于Annotation与SpringAOP的缓存简单解决方案

    前言: 由于项目的原因,需要对项目中大量访问多修改少的数据进行缓存并管理,为达到开发过程中通过Annotation简单的配置既可以完成对缓存的设置与更新的需求,故而设计的该简易的解决方案. 涉及技术: ...

  2. SpringMVC + ehcache( ehcache-spring-annotations)基于注解的服务器端数据缓存

    背景 声明,如果你不关心java缓存解决方案的全貌,只是急着解决问题,请略过背景部分. 在互联网应用中,由于并发量比传统的企业级应用会高出很多,所以处理大并发的问题就显得尤为重要.在硬件资源一定的情况 ...

  3. spring-aop + memcached 的简单实现

    一般情况下,java程序取一条数据是直接从数据库中去取,当数据库达到一定的连接数时,就会处于排队等待状态,某些在一定时间内不会发生变化的数据,完全没必要每次都从数据库中去取,使用spring-aop ...

  4. 基于.net的分布式系统限流组件 C# DataGridView绑定List对象时,利用BindingList来实现增删查改 .net中ThreadPool与Task的认识总结 C# 排序技术研究与对比 基于.net的通用内存缓存模型组件 Scala学习笔记:重要语法特性

    基于.net的分布式系统限流组件   在互联网应用中,流量洪峰是常有的事情.在应对流量洪峰时,通用的处理模式一般有排队.限流,这样可以非常直接有效的保护系统,防止系统被打爆.另外,通过限流技术手段,可 ...

  5. Spring Aop(二)——基于Aspectj注解的Spring Aop简单实现

    转发地址:https://www.iteye.com/blog/elim-2394762 2 基于Aspectj注解的Spring Aop简单实现 Spring Aop是基于Aop框架Aspectj实 ...

  6. 【开源项目系列】如何基于 Spring Cache 实现多级缓存(同时整合本地缓存 Ehcache 和分布式缓存 Redis)

    一.缓存 当系统的并发量上来了,如果我们频繁地去访问数据库,那么会使数据库的压力不断增大,在高峰时甚至可以出现数据库崩溃的现象.所以一般我们会使用缓存来解决这个数据库并发访问问题,用户访问进来,会先从 ...

  7. 基于 Annotation 拦截的 Spring AOP 权限验证方法

    基于 Annotation 拦截的 Spring AOP 权限验证方法 转自:http://www.ibm.com/developerworks/cn/java/j-lo-springaopfilte ...

  8. Struts2中基于Annotation的细粒度权限控制

    Struts2中基于Annotation的细粒度权限控制 2009-10-19 14:25:53|  分类: Struts2 |  标签: |字号大中小 订阅     权限控制是保护系统安全运行很重要 ...

  9. iOS开发--基于AFNetWorking3.0的图片缓存分析

    图片在APP中占有重要的角色,对图片做好缓存是重要的一项工作.[TOC] 理论 不喜欢理论的可以直接跳到下面的Demo实践部分 缓存介绍 缓存按照保存位置可以分为两类:内存缓存.硬盘缓存(FMDB.C ...

随机推荐

  1. find unique values in an array

    Problem: given an array that contains duplicates (except one value), find the one value that does no ...

  2. 在Windows上安装Python

    首先,从官网下载 最新版本 的Python 2.7.可通过 Python官网 的”Windows Installer”链接保证下载到的版本是最新的. Windows版本是MSI文件格式,双击它即可开始 ...

  3. 详细的SQL中datediff用法

    DATEDIFF 函数 [日期和时间] 功能返回两个日期之间的间隔. 语法DATEDIFF ( date-part, date-expression-1, date-expression-2 ) da ...

  4. git 使用系列(二)---- 分支和合并

    Branching and Merging The Git feature that really makes it stand apart from nearly every other SCM o ...

  5. Too Much Money

    Too Much Money time limit per test 2 seconds memory limit per test 256 megabytes input standard inpu ...

  6. oracle中的exists 和in

    有两个简单例子,以说明 “exists”和“in”的效率问题 1) select * from T1 where exists(select 1 from T2 where T1.a=T2.a) ; ...

  7. Zookeeper: configuring on centos7

    thispassage is referenced, appreciated. ZooKeeper installation: Download from this site Install java ...

  8. DZY Loves Partition

    问题描述 DZY喜欢拆分数字.他想知道能否把nn拆成恰好kk个不重复的正整数之和. 思考了一会儿之后他发现这个题太简单,于是他想要最大化这kk个正整数的乘积.你能帮帮他吗? 由于答案可能很大,请模10 ...

  9. 快学Scala-第二章 控制结构和函数

    知识点: 1.条件表达式 if(x>0) 1 else 0 scala每个表达式都有一个类型,如果一致则为它们的类型,如果为混合类型表达式,则为它们的公共超类型Any. if(x>0) 1 ...

  10. shell 分词

    ######################################################################### # File Name: hello.sh # Au ...