Java 动态代理
被代理的接口特点:
1. 不能有重复的接口,以避免动态代理类代码生成时的编译错误。
2. 这些接口对于类装载器必须可见,否则类装载器将无法链接它们,将会导致类定义失败。
3. 需被代理的所有非 public 的接口必须在同一个包中,否则代理类生成也会失败。
4. 接口的数目不能超过 65535,这是 JVM 设定的限制。
动态生成的代理类的特点:
1)包:如果所代理的接口都是 public 的,那么它将被定义在顶层包(即包路径为空),如果所代理的接口中有非 public 的接口(因为接口不能被定义为 protect 或 private,所以除 public 之外就是默认的 package 访问级别),那么它将被定义在该接口所在包(假设代理了 com.ibm.developerworks 包中的某非 public 接口 A,那么新生成的代理类所在的包就是 com.ibm.developerworks),这样设计的目的是为了最大程度的保证动态代理类不会因为包管理的问题而无法被成功定义并访问;
2)类修饰符:该代理类具有 final 和 public 修饰符,意味着它可以被所有的类访问,但是不能被再度继承;
3)类名:格式是“$ProxyN”,其中 N 是一个逐一递增的阿拉伯数字,代表 Proxy 类第 N 次生成的动态代理类,值得注意的一点是,并不是每次调用 Proxy 的静态方法创建动态代理类都会使得 N 值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会很聪明地返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率。
4)类继承关系:该类的继承关系如图:
由图可见,Proxy 类是它的父类,这个规则适用于所有由 Proxy 创建的动态代理类。而且该类还实现了其所代理的一组接口,这就是为什么它能够被安全地类型转换到其所代理的某接口的根本原因。
代理类实例的一些特点:
1. 每个实例都会关联一个调用处理器对象(InvocationHandler),可以通过 Proxy 提供的静态方法 getInvocationHandler 去获得代理类实例的调用处理器对象。
2. 当代理的一组接口有重复声明的方法且该方法被调用时,代理类总是从排在最前面的接口中获取方法对象并分派给调用处理器,而无论代理类实例是否正在以该接口(或继承于该接口的某子接口)的形式被外部引用,因为在代理类内部无法区分其当前的被引用类型。
* 注明: 在代理类实例上调用其代理的接口中所声明的方法时,这些方法最终都会由调用处理器的 invoke 方法执行,此外,代理类的根类 java.lang.Object 中的hashCode,equals 和 toString三个方法也同样会被分派到调用处理器的 invoke 方法执行,可能的原因有:
1. 因为这些方法为 public 且非 final 类型,能够被代理类覆盖;
2. 因为这些方法往往呈现出一个类的某种特征属性,具有一定的区分度,所以为了保证代理类与委托类对外的一致性,这三个方法也应该被分派到委托类执行。
代理:设计模式
代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。
图 1. 代理模式
为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。Java 动态代理机制以巧妙的方式近乎完美地实践了代理模式的设计理念。
相关的类和接口
java.lang.reflect.Proxy:这是 Java 动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。
java.lang.reflect.InvocationHandler:这是调用处理器接口,它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问。
java.lang.ClassLoader:这是类装载器类,负责将类的字节码装载到 Java 虚拟机(JVM)中并为其定义类对象,然后该类才能被使用。Proxy 静态方法生成动态代理类同样需要通过类装载器来进行装载才能使用,它与普通类的唯一区别就是其字节码是由 JVM 在运行时动态生成的而非预存在于任何一个 .class 文件中。
代理机制及其特点
如何使用 Java 动态代理。具体有如下四步骤:
- 1. 通过实现 InvocationHandler 接口创建自己的调用处理器;
- 2. 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
- 3. 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
- 4. 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
/********动态代理过程***********/ // InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发
// 其内部通常包含指向委托类实例的引用,用于真正执行分派转发过来的方法调用
InvocationHandler handler = new InvocationHandlerImpl(..); // 通过 Proxy 为包括 Interface 接口在内的一组接口动态创建代理类的类对象
Class clazz = Proxy.getProxyClass(classLoader, new Class[] { Interface.class, ... }); // 通过反射从生成的类对象获得构造函数对象
Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class }); // 通过构造函数对象创建动态代理类实例
Interface Proxy = (Interface)constructor.newInstance(new Object[] { handler });
实际使用过程更加简单,因为 Proxy 的静态方法 newProxyInstance 已经为我们封装了步骤 2 到步骤 4 的过程,所以简化后的过程如下:
// InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发
InvocationHandler handler = new InvocationHandlerImpl(..); // 通过 Proxy 直接创建动态代理类实例
Interface proxy = (Interface)Proxy.newProxyInstance( classLoader,
new Class[] { Interface.class },
handler );
简单的demo:
Hello接口:
public interface Hello {
void sayHello(String to);
void print(String p);
}
Hello接口的实现类:
public class HelloImpl implements Hello { public void sayHello(String to) {
System.out.println("Say hello to " + to);
} public void print(String s) {
System.out.println("print : " + s);
} }
与代理类(HelloImpl类)相关联的InvocationHandler对象
public class LogHandler implements InvocationHandler { private Object dele; public LogHandler(Object obj) {
this.dele = obj;
} public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
doBefore();
//在这里完全可以把下面这句注释掉,而做一些其它的事情
Object result = method.invoke(dele, args);
after();
return result;
} private void doBefore() {
System.out.println("before....");
} private void after() {
System.out.println("after....");
}
}
最后测试代码如下:
public class ProxyTest { public static void main(String[] args) {
HelloImpl impl = new HelloImpl();
LogHandler handler = new LogHandler(impl);
//这里把handler与impl新生成的代理类相关联
Hello hello = (Hello) Proxy.newProxyInstance(impl.getClass().getClassLoader(), impl.getClass().getInterfaces(), handler); //这里无论访问哪个方法,都是会把请求转发到handler.invoke
hello.print("All the test");
hello.sayHello("Denny");
} }
Proxy 静态方法 newProxyInstance
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException { // 检查 h 不为空,否则抛异常
if (h == null) {
throw new NullPointerException();
} // 获得与制定类装载器和一组接口相关的代理类类型对象
Class cl = getProxyClass(loader, interfaces); // 通过反射获取构造函数对象并生成代理类实例
try {
Constructor cons = cl.getConstructor(constructorParams);
return (Object) cons.newInstance(new Object[] { h });
} catch (NoSuchMethodException e) { throw new InternalError(e.toString());
} catch (IllegalAccessException e) { throw new InternalError(e.toString());
} catch (InstantiationException e) { throw new InternalError(e.toString());
} catch (InvocationTargetException e) { throw new InternalError(e.toString());
}
}
动态代理真正的关键是在 getProxyClass 方法,该方法负责为一组接口动态地生成代理类类型对象。该方法总共可以分为四个步骤:
1. 对这组接口进行一定程度的安全检查。
2. 从 loaderToCache 映射表中获取以类装载器对象为关键字所对应的缓存表。
3. 动态创建代理类的类对象。
4. 代码生成过程进入结尾部分,更新缓存表,唤醒其他等待进程。
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
} Class<?> proxyClass = null; /* 存储interface 名称,用来作为proxy class cache的键值key */
String[] interfaceNames = new String[interfaces.length]; // 用来确认是否有重复接口
Set<Class<?>> interfaceSet = new HashSet<>(); for (int i = 0; i < interfaces.length; i++) { String interfaceName = interfaces[i].getName();
Class<?> interfaceClass = null;
try {
/*
*检查接口类对象是否对类装载器可见
*/
interfaceClass = Class.forName(interfaceName, false, loader);
} catch (ClassNotFoundException e) {
}
/*
*检查接口类对象与类装载器所能识别的接口类对象是完全相同的
*/
if (interfaceClass != interfaces[i]) {
throw new IllegalArgumentException(
interfaces[i] + " is not visible from class loader");
} /*
* 确保是 interface 类型而不是 class 类型
*/
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
} /*
* 确保接口不重复
*/
if (interfaceSet.contains(interfaceClass)) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
interfaceSet.add(interfaceClass); interfaceNames[i] = interfaceName;
} /*
* 从 loaderToCache 映射表中获取以类装载器对象为关键字所对应的缓存表,
* 如果不存在就创建一个新的缓存表并更新到 loaderToCache。缓存表是一个 HashMap 实例,
* 正常情况下它将存放键值对(接口名字列表,动态生成的代理类的类对象引用)。
* 当代理类正在被创建时它会临时保存(接口名字列表,pendingGenerationMarker)。
* 标记 pendingGenerationMarke 的作用是通知后续的同类请求(接口数组相同且组内接口排列顺序也相同)
* 代理类正在被创建,请保持等待直至创建完成。
*/
List<String> key = Arrays.asList(interfaceNames); /*
* 获取以类装载器对象为关键字所对应的缓存表
*/
Map<List<String>, Object> cache;
synchronized (loaderToCache) {
cache = loaderToCache.get(loader);
if (cache == null) {
cache = new HashMap<>();
loaderToCache.put(loader, cache);
}
} synchronized (cache) { do {
// 以接口名字列表作为关键字获得对应 cache 值
Object value = cache.get(key);
if (value instanceof Reference) {
proxyClass = (Class<?>) ((Reference) value).get();
}
if (proxyClass != null) {
// 如果已经创建,直接返回
return proxyClass;
} else if (value == pendingGenerationMarker) {
// 代理类正在被创建,保持等待
try {
cache.wait();
} catch (InterruptedException e) { }
// 等待被唤醒,继续循环并通过二次检查以确保创建完成,否则重新等待
continue;
} else {
// 标记代理类正在被创建
cache.put(key, pendingGenerationMarker);
// break 跳出循环已进入创建过程
break;
}
} while (true);
} try {
String proxyPkg = null; //动态生成的代理类所在的包,见代理类特点1 /*
* 记录所有非public接口所在的包,生成的代理类也将被定义在这个包下。
* 确认所有非public接口在相同的包下。
*/
for (int i = 0; i < interfaces.length; i++) {
int flags = interfaces[i].getModifiers();
if (!Modifier.isPublic(flags)) {
String name = interfaces[i].getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
} // 如果没有非public接口,则使用com.sun.proxy这个包名
if (proxyPkg == null) {
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
} {
/*
* 获取proxy的名字编号。见代理类特点3
*/
long num;
synchronized (nextUniqueNumberLock) {
num = nextUniqueNumber++;
}
String proxyName = proxyPkg + proxyClassNamePrefix + num;
//
private final static String proxyClassNamePrefix = "$Proxy";
/*
* 动态生成代理类的字节码数组
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces);
try {
// 动态地定义新生成的代理类
proxyClass = defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
throw new IllegalArgumentException(e.toString());
}
}
//把生成的代理类的类对象记录进 proxyClasses 表, isProxyClass方法将用来判断
proxyClasses.put(proxyClass, null); } finally {
/*
* 根据结果更新缓存表,如果成功则将代理类的类对象引用更新进缓存表,
* 否则清楚缓存表中对应关键值,最后唤醒所有可能的正在等待的线程。
*/
synchronized (cache) {
if (proxyClass != null) {
cache.put(key, new WeakReference<Class<?>>(proxyClass));
} else {
cache.remove(key);
}
cache.notifyAll();
}
}
return proxyClass;
}
给一个样例,最终动态生成的代理类proxyClassFile,写入文件并反编译之后的样子。
首先接口是Subject,实现类RealSubject,调用处理器ProxyHandler,生成的动态代理类是ProxySubject(实际动态生成的应该是$ProxyN。)
public interface Subject
{
public void doSomething();
}
public class RealSubject implements Subject
{
public void doSomething()
{
System.out.println( "call doSomething()" );
}
}
public class ProxyHandler implements InvocationHandler
{
private Object proxied; public ProxyHandler( Object proxied )
{
this.proxied = proxied;
} public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable
{
//在转调具体目标对象之前,可以执行一些功能处理 //转调具体目标对象的方法
return method.invoke( proxied, args); //在转调具体目标对象之后,可以执行一些功能处理
}
}
代理类的使用和写入ProxySubject.class文件,用以反编译出ProxySubject.java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import sun.misc.ProxyGenerator;
import java.io.*;
public class DynamicProxy
{
public static void main( String args[] )
{
RealSubject real = new RealSubject();
Subject proxySubject = (Subject)Proxy.newProxyInstance(Subject.class.getClassLoader(),
new Class[]{Subject.class},
new ProxyHandler(real)); proxySubject.doSomething(); //write proxySubject class binary data to file
createProxyClassFile();
} public static void createProxyClassFile()
{
String name = "ProxySubject";
byte[] data = ProxyGenerator.generateProxyClass( name, new Class[] { Subject.class } );
try
{
FileOutputStream out = new FileOutputStream( name + ".class" );
out.write( data );
out.close();
}
catch( Exception e )
{
e.printStackTrace();
}
}
}
编译出的ProxySubject.java
import java.lang.reflect.*;
public final class ProxySubject extends Proxy
implements Subject
{
private static Method m1;
private static Method m0;
private static Method m3;
private static Method m2;
public ProxySubject(InvocationHandler invocationhandler)
{
super(invocationhandler);
}
public final boolean equals(Object obj)
{
try
{
return ((Boolean)super.h.invoke(this, m1, new Object[] {
obj
})).booleanValue();
}
catch(Error _ex) { }
catch(Throwable throwable)
{
throw new UndeclaredThrowableException(throwable);
}
}
public final int hashCode()
{
try
{
return ((Integer)super.h.invoke(this, m0, null)).intValue();
}
catch(Error _ex) { }
catch(Throwable throwable)
{
throw new UndeclaredThrowableException(throwable);
}
}
public final void doSomething()
{
try
{
super.h.invoke(this, m3, null);
return;
}
catch(Error _ex) { }
catch(Throwable throwable)
{
throw new UndeclaredThrowableException(throwable);
}
}
public final String toString()
{
try
{
return (String)super.h.invoke(this, m2, null);
}
catch(Error _ex) { }
catch(Throwable throwable)
{
throw new UndeclaredThrowableException(throwable);
}
}
static
{
try
{
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {
Class.forName("java.lang.Object")
});
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
m3 = Class.forName("Subject").getMethod("doSomething", new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
}
catch(NoSuchMethodException nosuchmethodexception)
{
throw new NoSuchMethodError(nosuchmethodexception.getMessage());
}
catch(ClassNotFoundException classnotfoundexception)
{
throw new NoClassDefFoundError(classnotfoundexception.getMessage());
}
}
}
ProxyGenerator内部是如何生成class二进制数据,可以参考源代码。
private byte[] generateClassFile() {
/*
* Record that proxy methods are needed for the hashCode, equals,
* and toString methods of java.lang.Object. This is done before
* the methods from the proxy interfaces so that the methods from
* java.lang.Object take precedence over duplicate methods in the
* proxy interfaces.
*/
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);
/*
* Now record all of the methods from the proxy interfaces, giving
* earlier interfaces precedence over later ones with duplicate
* methods.
*/
for (int i = 0; i < interfaces.length; i++) {
Method[] methods = interfaces[i].getMethods();
for (int j = 0; j < methods.length; j++) {
addProxyMethod(methods[j], interfaces[i]);
}
}
/*
* For each set of proxy methods with the same signature,
* verify that the methods' return types are compatible.
*/
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
checkReturnTypes(sigmethods);
}
/* ============================================================
* Step 2: Assemble FieldInfo and MethodInfo structs for all of
* fields and methods in the class we are generating.
*/
try {
methods.add(generateConstructor());
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
for (ProxyMethod pm : sigmethods) {
// add static field for method's Method object
fields.add(new FieldInfo(pm.methodFieldName,
"Ljava/lang/reflect/Method;",
ACC_PRIVATE | ACC_STATIC));
// generate code for proxy method and add it
methods.add(pm.generateMethod());
}
}
methods.add(generateStaticInitializer());
} catch (IOException e) {
throw new InternalError("unexpected I/O Exception");
}
/* ============================================================
* Step 3: Write the final class file.
*/
/*
* Make sure that constant pool indexes are reserved for the
* following items before starting to write the final class file.
*/
cp.getClass(dotToSlash(className));
cp.getClass(superclassName);
for (int i = 0; i < interfaces.length; i++) {
cp.getClass(dotToSlash(interfaces[i].getName()));
}
/*
* Disallow new constant pool additions beyond this point, since
* we are about to write the final constant pool table.
*/
cp.setReadOnly();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
try {
/*
* Write all the items of the "ClassFile" structure.
* See JVMS section 4.1.
*/
// u4 magic;
dout.writeInt(0xCAFEBABE);
// u2 minor_version;
dout.writeShort(CLASSFILE_MINOR_VERSION);
// u2 major_version;
dout.writeShort(CLASSFILE_MAJOR_VERSION);
cp.write(dout); // (write constant pool)
// u2 access_flags;
dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);
// u2 this_class;
dout.writeShort(cp.getClass(dotToSlash(className)));
// u2 super_class;
dout.writeShort(cp.getClass(superclassName));
// u2 interfaces_count;
dout.writeShort(interfaces.length);
// u2 interfaces[interfaces_count];
for (int i = 0; i < interfaces.length; i++) {
dout.writeShort(cp.getClass(
dotToSlash(interfaces[i].getName())));
}
// u2 fields_count;
dout.writeShort(fields.size());
// field_info fields[fields_count];
for (FieldInfo f : fields) {
f.write(dout);
}
// u2 methods_count;
dout.writeShort(methods.size());
// method_info methods[methods_count];
for (MethodInfo m : methods) {
m.write(dout);
}
// u2 attributes_count;
dout.writeShort(0); // (no ClassFile attributes for proxy classes)
} catch (IOException e) {
throw new InternalError("unexpected I/O Exception");
}
return bout.toByteArray();
参考:http://www.cnblogs.com/flyoung2008/archive/2013/08/11/3251148.html
更好的理解,可以参考:https://my.oschina.net/lyp3314/blog/136589
http://www.360doc.com/content/14/0313/17/16070877_360315087.shtml
Java 动态代理的更多相关文章
- Java 动态代理机制详解
在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来说,我们不但要知道怎么通过AOP来满足的 ...
- Java动态代理全面分析
代理模式 解说:给某一个对象提供一个代理,并由代理对象控制对原对象的引用: 代理模式需要以下几个角色: 1 主题:规定代理类和真实对象共同对外暴露的接口: 2 代理类:专门代理真实对象的类: 3 ...
- JAVA动态代理模式(从现实生活角度理解代码原理)
所谓动态代理,即通过代理类:Proxy的代理,接口和实现类之间可以不直接发生联系,而可以在运行期(Runtime)实现动态关联. java动态代理主要是使用java.lang.reflect包中的两个 ...
- Java 动态代理作用是什么?
Java 动态代理作用是什么? 1 条评论 分享 默认排序按时间排序 19 个回答 133赞同反对,不会显示你的姓名 Intopass 程序员,近期沉迷于动漫ING 133 人赞同 ① 首先你 ...
- java动态代理原理
我们经常会用到Java的动态代理技术, 虽然会使用, 但是自己对其中的原理却不是很了解.比如代理对象是如何产生的, InvocationHandler的invoke方法是如何调用的?今天就来深究下Ja ...
- java 动态代理示例,带主要注释
Java proxy是基于反射,仅仅支持基于接口的动态代理. java 动态代理是一切架构的基础,必须了解. 废话少说,先上代码获得感性认识. 示例代码有主要注释. 接口: public interf ...
- java动态代理浅析
最近在公司看到了mybatis与spring整合中MapperScannerConfigurer的使用,该类通过反向代理自动生成基于接口的动态代理类. 于是想起了java的动态代理,然后就有了这篇文章 ...
- [转]java动态代理(JDK和cglib)
转自:http://www.cnblogs.com/jqyp/archive/2010/08/20/1805041.html java动态代理(JDK和cglib) JAVA的动态代理 代理模式 代理 ...
- java动态代理模式
java动态代理机制详解 Spring的核心AOP的原理就是java的动态代理机制. 在java的动态代理机制中,有两个重要的类或接口: 1.InvocationHandler(Interface): ...
随机推荐
- Spring学习笔记—最小化Spring XML配置
自动装配(autowiring)有助于减少甚至消除配置<property>元素和<constructor-arg>元素,让Spring自动识别如何装配Bean的依赖关系. 自动 ...
- 基于SNMP的路由拓扑发现算法收集
一.三层(网络层)发现 算法来源:王娟娟.基于SNMP的网络拓扑发现算法研究.武汉科技大学硕士学位论文,2008 数据结构: 待检路由设备网关链表:存放指定深度内待检路由设备的网关信息,处理后删除. ...
- 与你相遇好幸运,Linux常用命令
开机挂载硬盘 cat /etc/fstab /dev/sda1 /mnt/sda1 ext3 defaults 0 0 挂载硬盘 mount /dev/sdb5 /home/dis ...
- xml解析方法总结
==========================================xml文件<?xml version=”1.0″ encoding=”GB2312″?> <RES ...
- [LeetCode] Largest Rectangle in Histogram
Given n non-negative integers representing the histogram's bar height where the width of each bar is ...
- SQL Server 2014 BI新特性(三)Power Query和Power Map功能预览
Power Query和Power Map是微软前不久在WPC上发布的Power BI中新的针对Excel的功能.借助这两样功能,自助式BI将更方便你发现和处理数据并且丰富数据的可视化功能. Powe ...
- the last lecture
2008.07.25,CMU教授Randy Pausch教授因癌症去世,仅47岁. 几年之前,当我看到Pausch先生最后一课的视频时,让我震撼. 转眼之间,7年过去了,这7年,让我成长了许多. 7年 ...
- MOS X 下Apache服务器配置,及日志读取
A01-配置Apache 在当前用户的目录创建一个文件夹 打开finder进入/etc/apache2/etc/apache2 是系统目录,默认不显示 进入该目录有两种方法 i. 显示所有隐藏和系统目 ...
- Java学习笔记(三)——运算符
一.运算符: 1.分类: 2.java中的运算符 (1)其中,++在左,表示先加了再用,++在右,表示先用了再加. (2)% 用来求余数,也称为"取模运算符" 3.赋值运算符 4. ...
- Socket编程基础知识
端口号常识: 端口号被从1 开始分配. 通常端口号超出255 的部分被本地主机保留为私有用途. 1到255 之间的号码被用于远程应用程序所请求的进程和网络服务. 每个网络通信循环地 ...