 |
 |
<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- 03/20/06 updated by gretchen -->
<!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
|
级别: 初级
Dennis Sosnoski, Java 和 XML 顾问, Sosnoski Software Solutions, Inc.
2005 年 6 月 01 日
您是否厌倦了为所有的数据类构建和维护 toString() 方法?在本期的 Classworking 工具箱文章中,Dennis Sosnoski 顾问向您展示了如何使用 J2SE 5.0 注释和 ASM 字节码操作框架来自动化该过程。他使用新增的 J2SE 5.0 instrumentation API,来在类被载入 JVM 中时调用 ASM,以提供运行时的动态类修改。
<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
到 J2SE 5.0,Sun 已经给 Java 平台添加了许多新特性。最为重要的一个新特性是支持注释。注释在关联多种类型的元数据与 Java 代码方面将会很有用,并且在扩展 Java 平台的新的和更新的 JSR 中,它已经被广泛用来代替定制配置文件。在本文中,我将向您展示如何结合使用 ASM 字节码操作框架和 J2SE 5.0 的新增特性 —— instrumentation 包 —— 来在类被加载到 JVM 中时,按照注释的控制来转换类。
注释基础知识
讨论 J2SE 5.0 注释的文章已经很多了(参阅 参考资料),所以在此我只作一个简短的归纳。注释是一种针对 Java 代码的元数据。在功能上类似于日益普及的用于处理复杂框架配置的 XDoclet 样式的元数据,而其实现则与 C# 属性有更多的共同点。
该语言特性的 Java 实现使用一种类似于接口的结构和 Java 语言语法的一些特殊扩展。我发现大多数情况下忽略这种类似于接口的结构,而把注释看作是名值对的 hashmap 会更清晰。每个注释类型定义了一组与之关联的固定名称。每个名称可能被赋予一个默认值,否则的话每次使用该注释时都要定义该名称。注释可以被指定应用于一种特定类型的 Java 组件(如类、字段、方法,等等),甚至还可以应用于其他的注释。(实际上,您是通过在要限制的注释的定义上使用一个特殊的预定义注释,来限制注释适用的组件的。)
不同于常规接口,注释必须在定义中使用关键字 @interface 。同样不同于常规接口的是,注释只能定义不带参数且只返回简单值(基本类型、String 、 Class 、enum 类型、注释,以及任意这些类型的数组)的“方法”。这些“方法”是与注释关联的值的名称。
注释被用作声明时的修饰符,就像 public 、final ,以及其他早于 J2SE 5.0 版本的 Java 语言所定义的关键字修饰符。注释的使用是由 @ 符号后面跟注释名来表明的。如果要给注释赋值,在注释名后面的圆括号中以名值对的形式给出。
清单 1 展示了一个示例注释声明,后面是将该注释用于某些方法的类的定义。该 LogMe 注释用来标记应该包含在应用程序的日志记录中的方法。我已经给该注释赋了两个值:一个表示该调用被包含其中的日志记录的级别,另一个表示用于该方法调用的名称(默认是空字符串,假定没有名称时,处理该注释的代码将代入实际的方法名)。然后我将该注释用于 StringArray 类中的两个方法,对 merge() 方法只使用默认值,对 indexOf() 方法则提供显式值。
清单 1. 反射代替接口及其实现
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
/**
* Annotation for method to be included in logging.
*/
@Target({ElementType.METHOD})
public @interface LogMe {
int level() default 0;
String name() default "";
}
public class StringArray
{
private final String[] m_list;
public StringArray(String[] list) {
...
}
public StringArray(StringArray base, String[] adds) {
...
}
@LogMe private String[] merge(String[] list1, String[]list2) {
...
}
public String get(int index) {
return m_list[index];
}
@LogMe(level=1, name="lookup") public int indexOf(String value) {
...
}
public int size() {
return m_list.length;
}
}
|
下一小节我将介绍一个不同的(我认为是更有趣的)应用程序。
构建 toString() 方法
Java 平台提供了一个方便的挂钩,以生成 toString() 方法形式的对象的文本描述。最终基类 java.lang.Object 提供了该方法的一个默认实现,但是仍鼓励重写默认实现以提供更有用的描述。许多开发人员习惯提供自己的实现,至少对于那些基本上是数据表示的类是这样。我要先承认我不是其中之一 —— 我常常认为 toString() 非常有用,一般不会费心去重写默认实现。为了更有用些,当从类中添加或删除字段时,toString() 实现需要保持最新。而我发现总的来说这一步太麻烦而不值得实现。
把注释与类文件修改组合起来可以提供一种走出这一困境的方法。我所遇到的维护 toString() 方法的问题是由于代码与类中的字段声明分离了,这意味着每次添加或删除字段时还有一个需要记得更改的东西。通过在字段声明时使用注释,可以很容易地表明想要在 toString() 方法中包含哪些字段,而把该方法的实际实现留给 classworking 工具。这样,所有的东西都在一个地方(字段声明中),而且获得了 toString() 的有用的描述而无需维护代码。
源代码示例
在实现 toString() 方法结构的注释之前,我将给出要实现的代码示例。清单 2 展示了源代码中包含 toString() 方法的示例数据保持类:
清单 2. 带有 toString() 方法的数据类
public class Address
{
private String m_street;
private String m_city;
private String m_state;
private String m_zip;
public Address() {}
public Address(String street, String city, String state, String zip) {
m_street = street;
m_city = city;
m_state = state;
m_zip = zip;
}
public String getCity() {
return m_city;
}
public void setCity(String city) {
m_city = city;
}
...
public String toString() {
StringBuffer buff = new StringBuffer();
buff.append("Address: street=");
buff.append(m_street);
buff.append(", city=");
buff.append(m_city);
buff.append(", state=");
buff.append(m_state);
buff.append(", zip=");
buff.append(m_zip);
return buff.toString();
}
}
|
对于清单 2 的示例,我选择在 toString() 输出中包含所有的字段,字段顺序与其在类中声明的顺序相同,并以“name=”文本来开始每个字段值,以在输出中标识它们。对于本例,文本是通过剥去用来标识成员字段的“m_”前缀,来直接从字段名生成的。在其他情况下,我可能想要在输出中仅包含某些字段、更改顺序、更改用于值的标识符文本,或者甚至完全跳过标识符文本。注释格式灵活得足以表示所有的可能。
定义注释
可以以多种方式为 toString() 的生成定义注释。为使它真正有用,我情愿最小化所需的注释数目,可能通过使用类注释来标志我想要在其中生成方法的类,并使用单个的字段注释来重写字段的默认处理。这并不太难做到,但是实现代码将变得相当复杂。对于本文来说,我想使它保持简单,因此只使用包含在实例的描述中的单个字段的注释。
我想要控制的因素有:要包含哪些字段,字段值是否有前导文本,该文本是否基于字段名,以及字段在输出中的顺序。清单 3 给出了一个针对该目的的基本注释:
清单 3. toString() 生成的注释
package com.sosnoski.asm;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target({ElementType.FIELD})
public @interface ToString {
int order() default 0;
String text() default "";
}
|
清单 3 的注释只定义了一对命名值,给出了顺序和用于一个字段的前导文本。我已经用 @Target 行将该注释的使用限定到字段声明。我还为每个值定义了默认值。这些默认值并不应用于成为二进制类表示的生成的注释信息(只有当注释在运行时作为伪接口被访问时,它们才应用,而我不会这么做),所以我实际上并不关心使用什么值。我只是通过定义默认值,使值是可选的,而不必在每次使用注释时都指定它们。
使用注释时要记住的一个因素是,命名值必须始终是编译时常量,而且不能为 null 。该规则适用于默认值(如果指定的话)和由用户设置的值。我猜测这个决定是基于与早期 Java 语言定义的一致性而做出的,但是我觉得奇怪的是,对 Java 语言做出如此重大修改的规范,却只局限于这一方面的一致性。
实现生成
既然已经打好了基础,就该研究实现 classworking 转换了:当载入带注释的类时向它们添加 toString() 方法。该实现涉及三个单独的代码段:截获 classloading、访问注释信息和实际转换。
用 instrumentation 来截获
J2SE 5.0 给 Java 平台添加了许多特性。就我个人而言,我并不认为所有这些添加的特性都是改进。但是,有两个不太引人注意的新特性确实对 classworking 很有用,就是 java.lang.instrument 包和 JVM 接口,它们使您可以指定将在执行程序时使用的类转换代理,当然还有其他功能。
要使用转换代理,需要在启动 JVM 时指定代理类。当使用 java 命令来运行 JVM 时,可以使用命令行参数,以 -javaagent:jarpath[=options] 的形式来指定代理,其中“jarpath”是到包含代理类的 JAR 文件的路径,而“options”是代理的参数串。代理 JAR 文件使用一个特殊的清单属性来指定实际的代理类,这必须定义一个方法: public static void premain(String options, Instrumentation inst) 。 该代理 premain() 方法将先于应用程序的 main() 方法调用,而且能够使用传入的 java.lang.instrument.Instrumentation 类实例注册实际的转换器。
该转换器类必须实现 java.lang.instrument.ClassFileTransformer 接口,后者定义了一个 transform() 方法。当使用 Instrumentation 类实例注册一个转换器实例时,将会为在 JVM 中创建的每个类调用该转换器实例。转换器将获得到二进制类表示的访问,并且可以在类表示被 JVM 加载之前修改它。
清单 4 给出了处理注释的代理和转换器类(在本例中是同一个类,但是这二者不一定要相同)实现。 transform() 实现使用 ASM 来扫描提供的二进制类表示,并寻找适当的注释,收集关于该类的带注释字段的信息。如果找到带注释的字段,该类将被修改以包含生成的 toString() 方法,而修改后的二进制表示将被返回。否则 transform() 方法只返回 null ,表明没有必要进行修改。
清单 4. 代理和转换器类
package com.sosnoski.asm;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
public class ToStringAgent implements ClassFileTransformer
{
// transformer interface implementation
public byte[] transform(ClassLoader loader, String cname, Class class,
ProtectionDomain domain, byte[] bytes)
throws IllegalClassFormatException {
System.out.println("Processing class " + cname);
try {
// scan class binary format to find fields for toString() method
ClassReader creader = new ClassReader(bytes);
FieldCollector visitor = new FieldCollector();
creader.accept(visitor, true);
FieldInfo[] fields = visitor.getFields();
if (fields.length > 0) {
// annotated fields present, generate the toString() method
System.out.println("Modifying " + cname);
ClassWriter writer = new ClassWriter(false);
ToStringGenerator gen = new ToStringGenerator(writer,
cname.replace('.', '/'), fields);
creader.accept(gen, false);
return writer.toByteArray();
}
} catch (IllegalStateException e) {
throw new IllegalClassFormatException("Error: " + e.getMessage() +
" on class " + cname);
}
return null;
}
// Required method for instrumentation agent.
public static void premain(String arglist, Instrumentation inst) {
inst.addTransformer(new ToStringAgent());
}
}
|
J2SE 5.0 的 instrumentation 特性远远不止是我在此所展示的,它包括访问加载到 JVM 中的所有类,甚至重定义已有类(如果 JVM 支持的话)的能力。对于本文,我将跳过其他的特性,继续来看用于处理注释和修改类的 ASM 代码。
累积元数据
ASM 2.0 使处理注释变得更容易了。正如您在 上个月的文章 中了解到的,ASM 使用 visitor 的方法来报告类数据的所有组件。J2SE 5.0 注释是使用 org.objectweb.asm.AnnotationVisitor 接口报告的。该接口定义了几个方法,其中我将只使用两个:visitAnnotation() 是处理注释时调用的方法,而 visit() 是处理注释的特定的名值对时调用的方法。我还需要实际字段信息,这是使用基本 org.objectweb.asm.ClassVisitor 接口中的 visitField() 方法报告的。
实现感兴趣的两个接口的所有方法将是冗长乏味的,但幸运的是 ASM 提供了一个方便的 org.objectweb.asm.commons.EmptyVisitor 类,作为编写自己的 visitor 的基础。EmptyVisitor 只是提供了所有不同种类的 visitor 的空的实现,允许您只对感兴趣的 visitor 方法建子类和重写。清单 5 给出了扩展 EmptyVisitor 类而得到的处理 ToString 注释的 FieldCollector 类。清单中也包含了用来保存收集的字段信息的 FieldInfo 类。
清单 5. 处理类的注释
package com.sosnoski.asm;
import java.util.ArrayList;
import java.util.Arrays;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.EmptyVisitor;
/**
* Visitor implementation to collect field annotation information from class.
*/
public class FieldCollector extends EmptyVisitor
{
private boolean m_isIncluded;
private int m_fieldAccess;
private String m_fieldName;
private Type m_fieldType;
private int m_fieldOrder;
private String m_fieldText;
private ArrayList m_fields = new ArrayList();
// finish field handling, once we're past it
private void finishField() {
if (m_isIncluded) {
m_fields.add(new FieldInfo(m_fieldName, m_fieldType,
m_fieldOrder, m_fieldText));
}
m_isIncluded = false;
}
// return array of included field information
public FieldInfo[] getFields() {
finishField();
FieldInfo[] infos =
(FieldInfo[])m_fields.toArray(new FieldInfo[m_fields.size()]);
Arrays.sort(infos);
return infos;
}
// process field found in class
public FieldVisitor visitField(int access, String name, String desc,
String sig, Object init) {
// finish processing of last field
finishField();
// save information for this field
m_fieldAccess = access;
m_fieldName = name;
m_fieldType = Type.getReturnType(desc);
m_fieldOrder = Integer.MAX_VALUE;
// default text is empty if non-String object, otherwise from field name
if (m_fieldType.getSort() == Type.OBJECT &&
!m_fieldType.getClassName().equals("java.lang.String")) {
m_fieldText = "";
} else {
String text = name;
if (text.startsWith("m_") && text.length() > 2) {
text = Character.toLowerCase(text.charAt(2)) +
text.substring(3);
}
m_fieldText = text;
}
return super.visitField(access, name, desc, sig, init);
}
// process annotation found in class
public AnnotationVisitor visitAnnotation(String sig, boolean visible) {
// flag field to be included in representation
if (sig.equals("Lcom/sosnoski/asm/ToString;")) {
if ((m_fieldAccess & Opcodes.ACC_STATIC) == 0) {
m_isIncluded = true;
} else {
throw new IllegalStateException("ToString " +
"annotation is not supported for static field +" +
" m_fieldName");
}
}
return super.visitAnnotation(sig, visible);
}
// process annotation name-value pair found in class
public void visit(String name, Object value) {
// ignore anything except the pair defined for toString() use
if ("order".equals(name)) {
m_fieldOrder = ((Integer)value).intValue();
} else if ("text".equals(name)) {
m_fieldText = value.toString();
}
}
}
package com.sosnoski.asm;
import org.objectweb.asm.Type;
/**
* Information for field value to be included in string representation.
*/
public class FieldInfo implements Comparable
{
private final String m_field;
private final Type m_type;
private final int m_order;
private final String m_text;
public FieldInfo(String field, Type type, int order,
String text) {
m_field = field;
m_type = type;
m_order = order;
m_text = text;
}
public String getField() {
return m_field;
}
public Type getType() {
return m_type;
}
public int getOrder() {
return m_order;
}
public String getText() {
return m_text;
}
/* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
public int compareTo(Object comp) {
if (comp instanceof FieldInfo) {
return m_order - ((FieldInfo)comp).m_order;
} else {
throw new IllegalArgumentException("Wrong type for comparison");
}
}
}
|
清单 5 的代码保存了访问字段时的字段信息,因为如果该字段有注释呈现的话,以后将会需要该信息。当访问注释时,该代码审查它是否是 ToString 注释,如果是,设置一个标志,说明当前字段应该被包含在用于生成 toString() 方法的列表中。当访问一个注释名值对时,该代码审查由 ToString 注释定义的两个名称,当找到时,保存每个名称的值。这些名称的真正默认值(与在注释定义中使用的默认值相对)是在字段的 visitor 方法中设置的,所以任意由用户指定的值都将重写这些默认值。
ASM 首先访问字段,接着访问注释和注释值。因为在处理字段的注释时,没有特定的方法可以调用,所以当处理一个新字段和当需要字段的完成列表时,我会调用一个 finishField() 方法。getFields() 方法向调用者提供字段的完成列表,以由注释值所确定的顺序排列。
转换类
清单 6 展示了实现代码的最后部分,它实际上向类添加了 toString() 方法。该代码与 上个月的文章 中使用 ASM 构造一个类的代码类似,但是需要另外构造以修改一个已有的类。这里,ASM 使用的 visitor 方法增加了复杂性 —— 要修改一个已有的类,需要访问所有的当前类目录,并把它传递给类编写者。org.objectweb.asm.ClassAdapter 是针对此目的的一个方便的基类。它实现了对提供的类编写者实例的传递处理,使您可以只重写需要特殊处理的方法。
清单 6. 添加 toString() 方法
package com.sosnoski.asm;
import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
/**
* Visitor to add <code>toString</code> method to a class.
*/
public class ToStringGenerator extends ClassAdapter
{
private final ClassWriter m_writer;
private final String m_internalName;
private final FieldInfo[] m_fields;
public ToStringGenerator(ClassWriter cw, String iname, FieldInfo[] props) {
super(cw);
m_writer = cw;
m_internalName = iname;
m_fields = props;
}
// called at end of class
public void visitEnd() {
// set up to build the toString() method
MethodVisitor mv = m_writer.visitMethod(Opcodes.ACC_PUBLIC,
"toString", "()Ljava/lang/String;", null, null);
mv.visitCode();
// create and initialize StringBuffer instance
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuffer");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuffer",
"<init>", "()V");
// start text with class name
String name = m_internalName;
int split = name.lastIndexOf('/');
if (split >= 0) {
name = name.substring(split+1);
}
mv.visitLdcInsn(name + ":");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuffer",
"append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;");
// loop through all field values to be included
boolean newline = false;
for (int i = 0; i < m_fields.length; i++) {
// check type of field (objects other than Strings need conversion)
FieldInfo prop = m_fields[i];
Type type = prop.getType();
boolean isobj = type.getSort() == Type.OBJECT &&
!type.getClassName().equals("java.lang.String");
// format lead text, with newline for object or after object
String lead = (isobj || newline) ? "/n " : " ";
if (prop.getText().length() > 0) {
lead += prop.getText() + "=";
}
mv.visitLdcInsn(lead);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuffer", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuffer;");
// load the actual field value and append
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, m_internalName,
prop.getField(), type.getDescriptor());
if (isobj) {
// convert objects by calling toString() method
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
type.getInternalName(), "toString",
"()Ljava/lang/String;");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuffer", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuffer;");
} else {
// append other types directly to StringBuffer
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"java/lang/StringBuffer", "append", "(" +
type.getDescriptor() + ")Ljava/lang/StringBuffer;");
}
newline = isobj;
}
// finish the method by returning accumulated text
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuffer",
"toString", "()Ljava/lang/String;");
mv.visitInsn(Opcodes.ARETURN);
mv.visitMaxs(3, 1);
mv.visitEnd();
super.visitEnd();
}
}
|
在清单 6 中,需要重写的惟一方法就是 visitEnd() 方法。该方法在所有的已有类信息都已经被访问之后调用,所以它对于添加新内容非常方便。我已经用 visitEnd() 方法向正在处理的类添加 toString() 方法。在代码生成中,我已经添加了一些用于精密地格式化 toString() 输出的特性,但是基本原理很简单 —— 只是循环遍历字段数组,生成代码,该代码首先向 StringBuffer 实例追加前导文本,然后追加实际字段值。
因为当前的代码将只使用 J2SE 5.0(由于使用了 instrumentation 方法来截获 classloading),所以我本应该使用新的 StringBuilder 类作为 StringBuffer 的更有效的等价物。我之所以选择使用以前的方案,是因为下一篇文章中我将使用该代码进行一些后续工作,但是您应该记住 StringBuilder 以便用于您自己的特定于 J2SE 5.0 的代码。
运行 ToString
清单 7 展示了 ToString 注释的一些测试类。我对实际注释使用了混合样式,在一些情况中指定了名值对,而其他的则只使用注释本身。Run 类创建带示例数据的 Customer 类实例,并打印出 toString() 方法调用的结果。
清单 7. ToString 的测试类
package com.sosnoski.dwct;
import com.sosnoski.asm.ToString;
public class Customer
{
@ToString(order=1, text="#") private long m_number;
@ToString() private String m_homePhone;
@ToString() private String m_dayPhone;
@ToString(order=2) private Name m_name;
@ToString(order=3) private Address m_address;
public Customer() {}
public Customer(long number, Name name, Address address, String homeph,
String dayph) {
m_number = number;
m_name = name;
m_address = address;
m_homePhone = homeph;
m_dayPhone = dayph;
}
...
}
...
public class Address
{
@ToString private String m_street;
@ToString private String m_city;
@ToString private String m_state;
@ToString private String m_zip;
public Address() {}
public Address(String street, String city, String state, String zip) {
m_street = street;
m_city = city;
m_state = state;
m_zip = zip;
}
public String getCity() {
return m_city;
}
public void setCity(String city) {
m_city = city;
}
...
}
...
public class Name
{
@ToString(order=1, text="") private String m_first;
@ToString(order=2, text="") private String m_middle;
@ToString(order=3, text="") private String m_last;
public Name() {}
public Name(String first, String middle, String last) {
m_first = first;
m_middle = middle;
m_last = last;
}
public String getFirst() {
return m_first;
}
public void setFirst(String first) {
m_first = first;
}
...
}
...
public class Run
{
public static void main(String[] args) {
Name name = new Name("Dennis", "Michael", "Sosnoski");
Address address = new Address("1234 5th St.", "Redmond", "WA", "98052");
Customer customer = new Customer(12345, name, address,
"425 555-1212", "425 555-1213");
System.out.println(customer);
}
}
|
最后,清单 8 展示了测试运行的控制台输出(首行被折行以适合屏幕):
清单 8. 测试运行的控制台输出(首行被折行)
[dennis@notebook code]$ java -cp lib/asm-2.0.RC1.jar:lib/asm-commons-2.0.RC1.jar
:lib/tostring-agent.jar:classes -javaagent:lib/tostring-agent.jar
com.sosnoski.dwct.Run
Processing class sun/misc/URLClassPath$FileLoader$1
Processing class com/sosnoski/dwct/Run
Processing class com/sosnoski/dwct/Name
Modifying com/sosnoski/dwct/Name
Processing class com/sosnoski/dwct/Address
Modifying com/sosnoski/dwct/Address
Processing class com/sosnoski/dwct/Customer
Modifying com/sosnoski/dwct/Customer
Customer: #=12345
Name: Dennis Michael Sosnoski
Address: street=1234 5th St. city=Redmond state=WA zip=98052
homePhone=425 555-1212 dayPhone=425 555-1213
|
结束语
我已经演示了如何使用 ASM 和 J2SE 5.0 注释来完成自动的运行时类文件修改。我用作例子的 ToString 注释是有趣而且(至少对于我来说)比较有用的。单独使用时,并不妨碍代码的可读性。但是注释如果被用于各种不同目的(这种情况将来肯定要发生,因为有如此多的 Java 扩展正在编写或重写以使用注释),就很有可能会影响代码的可读性。
当我在后面的文章中研究注释和外部配置文件的权衡时,我会再回到这个问题上。我个人的观点是,二者都有自己的作用,虽然注释基本上是作为配置文件的更容易的替代方案而开发的,但是独立的配置文件在某些情况下仍然适用。明确地讲,我认为 ToString 注释是一个适当使用的例子!
使用 J2SE 5.0 扩展的一个局限是 JDK 1.5 编译器输出只能与 JDK 1.5 JVM 一起使用。下一篇 Classworking 工具箱 文章,我将介绍一个克服该局限的工具,并展示如何修改 ToString 实现以运行在以前的JVM 上。
下载
描述
名字
大小
下载方法
Sample code |
j-cwt06075code.zip |
175 KB |
FTP
|
参考资料

|

|
关于作者
|
相关推荐
级联H桥SVG无功补偿系统在不平衡电网中的三层控制策略:电压电流双闭环PI控制、相间与相内电压均衡管理,级联H桥SVG无功补偿系统在不平衡电网中的三层控制策略:电压电流双闭环PI控制、相间与相内电压均衡管理,不平衡电网下的svg无功补偿,级联H桥svg无功补偿statcom,采用三层控制策略。 (1)第一层采用电压电流双闭环pi控制,电压电流正负序分离,电压外环通过产生基波正序有功电流三相所有H桥模块直流侧平均电压恒定,电流内环采用前馈解耦控制; (2)第二层相间电压均衡控制,注入零序电压,控制通过注入零序电压维持相间电压平衡; (3)第三层相内电压均衡控制,使其所有子模块吸收的有功功率与其损耗补,从而保证所有H桥子模块直流侧电压值等于给定值。 有参考资料。 639,核心关键词: 1. 不平衡电网下的SVG无功补偿 2. 级联H桥SVG无功补偿STATCOM 3. 三层控制策略 4. 电压电流双闭环PI控制 5. 电压电流正负序分离 6. 直流侧平均电压恒定 7. 前馈解耦控制 8. 相间电压均衡控制 9. 零序电压注入 10. 相内电压均衡控制 以上十个关键词用分号分隔的格式为:不
GTX 1080 PCB图纸,内含图纸查看软件
内容概要:本文档详细介绍了利用 DeepSeek 进行文本润色和问答交互时提高效果的方法和技巧,涵盖了从明确需求、提供适当上下文到尝试开放式问题以及多轮对话的十个要点。每一部分内容都提供了具体的示范案例,如指定回答格式、分步骤提问等具体实例,旨在指导用户更好地理解和运用 DeepSeek 提升工作效率和交流质量。同时文中还强调了根据不同应用场景调整提示词语气和风格的重要性和方法。 适用人群:适用于希望通过优化提问技巧以获得高质量反馈的企业员工、科研人员以及一般公众。 使用场景及目标:本文针对所有期望提高 DeepSeek 使用效率的人群,帮助他们在日常工作中快速获取精准的答案或信息,特别是在撰写报告、研究材料准备和技术咨询等方面。此外还鼓励用户通过不断尝试不同形式的问题表述来进行有效沟通。 其他说明:该文档不仅关注实际操作指引,同样重视用户思维模式转变——由简单索取答案向引导 AI 辅助创造性解决问题的方向发展。
基于FPGA与W5500实现的TCP网络通信测试平台开发——Zynq扩展口Verilog编程实践,基于FPGA与W5500芯片的TCP网络通信测试及多路Socket实现基于zynq开发平台和Vivado 2019软件的扩展开发,基于FPGA和W5500的TCP网络通信 测试平台 zynq扩展口开发 软件平台 vivado2019.2,纯Verilog可移植 测试环境 压力测试 cmd命令下ping电脑ip,同时采用上位机进行10ms发包回环测试,不丢包(内部数据回环,需要时间处理) 目前实现单socket功能,多路可支持 ,基于FPGA; W5500; TCP网络通信; Zynq扩展口开发; 纯Verilog可移植; 测试平台; 压力测试; 10ms发包回环测试; 单socket功能; 多路支持。,基于FPGA与W5500的Zynq扩展口TCP通信测试:可移植Verilog实现的高效网络通信
Labview液压比例阀伺服阀试验台多功能程序:PLC通讯、液压动画模拟、手动控制与调试、传感器标定、报警及记录、自动实验、数据处理与查询存储,报表生成与打印一体化解决方案。,Labview液压比例阀伺服阀试验台多功能程序:PLC通讯、液压动画模拟、手动控制与调试、传感器标定、报警管理及实验自动化,labview液压比例阀伺服阀试验台程序:功能包括,同PLC通讯程序,液压动画,手动控制及调试,传感器标定,报警设置及报警记录,自动实验,数据处理曲线处理,数据库存储及查询,报表自动生成及打印,扫码枪扫码及信号录入等~ ,核心关键词:PLC通讯; 液压动画; 手动控制及调试; 传感器标定; 报警设置及记录; 自动实验; 数据处理及曲线处理; 数据库存储及查询; 报表生成及打印; 扫码枪扫码。,Labview驱动的智能液压阀测试系统:多功能控制与数据处理
华为、腾讯、万科员工职业发展体系建设与实践.pptx
1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。
电网不对称故障下VSG峰值电流限制的柔性控制策略:实现电流平衡与功率容量的优化利用,电网不对称故障下VSG峰值电流限制的柔性控制策略:兼顾平衡电流与功率控制切换的动态管理,电网不对称故障下VSG峰值电流限制的柔性不平衡控制(文章完全复现)。 提出一种在不平衡运行条件下具有峰值电流限制的可变不平衡电流控制方法,可灵活地满足不同操作需求,包括电流平衡、有功或无功恒定运行(即电流控制、有功控制或无功控制之间的相互切),注入电流保持在安全值内,以更好的利用VSG功率容量。 关键词:VSG、平衡电流控制、有功功率控制、无功功率控制。 ,VSG; 峰值电流限制; 柔性不平衡控制; 电流平衡控制; 有功功率控制; 无功功率控制。,VSG柔性控制:在电网不对称故障下的峰值电流限制与平衡管理
1、文件内容:libpinyin-tools-0.9.93-4.el7.rpm以及相关依赖 2、文件形式:tar.gz压缩包 3、安装指令: #Step1、解压 tar -zxvf /mnt/data/output/libpinyin-tools-0.9.93-4.el7.tar.gz #Step2、进入解压后的目录,执行安装 sudo rpm -ivh *.rpm 4、更多资源/技术支持:公众号禅静编程坊
数据集是一个以经典动漫《龙珠》为主题的多维度数据集,广泛应用于数据分析、机器学习和图像识别等领域。该数据集由多个来源整合而成,涵盖了角色信息、战斗力、剧情片段、台词以及角色图像等多个方面。数据集的核心内容包括: 角色信息:包含《龙珠》系列中的主要角色及其属性,如名称、种族、所属系列(如《龙珠》《龙珠Z》《龙珠超》等)、战斗力等级等。 图像数据:提供角色的图像资源,可用于图像分类和角色识别任务。这些图像来自动画剧集、漫画和相关衍生作品。 剧情与台词:部分数据集还包含角色在不同故事中的台词和剧情片段,可用于文本分析和自然语言处理任务。 战斗数据:记录角色在不同剧情中的战斗力变化和战斗历史,为研究角色成长和剧情发展提供支持。 数据集特点 多样性:数据集整合了角色、图像、文本等多种类型的数据,适用于多种研究场景。 深度:不仅包含角色的基本信息,还涵盖了角色的成长历程、技能描述和与其他角色的互动关系。 实用性:支持多种编程语言(如Python、R)的数据处理和分析,提供了详细的文档和示例代码。
基于protues仿真的多功公交站播报系统设计(仿真图、源代码) 该设计为基于protues仿真的多功公交站播报系统,实现温度显示、时间显示、和系统公交站播报功能; 具体功能如下: 1、系统使用51单片机为核心设计; 2、时钟芯片进行时间和日期显示; 3、温度传感器进行温度读取; 4、LCD12864液晶屏进行相关显示; 5、按键设置调节时间; 6、按键设置报站; 7、仿真图、源代码; 操作说明: 1、下行控制报站:首先按下(下行设置按键),(下行指示灯)亮,然后按下(手动播报)按键控制播报下一站; 2、上行控制报站:首先按上(上行设置按键),(上行指示灯)亮,然后按下(手动播报)按键控制播报下一站; 3、按下关闭播报按键,则关闭播报功能和清除显示
采用Java后台技术和MySQL数据库,在前台界面为提升用户体验,使用Jquery、Ajax、CSS等技术进行布局。 系统包括两类用户:学生、管理员。 学生用户 学生用户只要实现了前台信息的查看,打开首页,查看网站介绍、琴房信息、在线留言、轮播图信息公告等,通过点击首页的菜单跳转到对应的功能页面菜单,包括网站首页、琴房信息、注册登录、个人中心、后台登录。 学生用户通过账户账号登录,登录后具有所有的操作权限,如果没有登录,不能在线预约。学生用户退出系统将注销个人的登录信息。 管理员通过后台的登录页面,选择管理员权限后进行登录,管理员的权限包括轮播公告管理、老师学生信息管理和信息审核管理,管理员管理后点击退出,注销登录信息。 管理员用户具有在线交流的管理,琴房信息管理、琴房预约管理。 在线交流是对前台用户留言内容进行管理,删除留言信息,查看留言信息。
MATLAB可以用于开发人脸识别考勤系统。下面是一个简单的示例流程: 1. 数据采集:首先收集员工的人脸图像作为训练数据集。可以要求员工提供多张照片以获得更好的训练效果。 2. 图像预处理:使用MATLAB的图像处理工具对采集到的人脸图像进行预处理,例如灰度化、裁剪、缩放等操作。 3. 特征提取:利用MATLAB的人脸识别工具包,如Face Recognition Toolbox,对处理后的图像提取人脸特征,常用的方法包括主成分分析(PCA)和线性判别分析(LDA)等。 4. 训练模型:使用已提取的人脸特征数据集训练人脸识别模型,可以选择支持向量机(SVM)、卷积神经网络(CNN)等算法。 5. 考勤系统:在员工打卡时,将摄像头捕获的人脸图像输入到训练好的模型中进行识别,匹配员工信息并记录考勤数据。 6. 结果反馈:根据识别结果,可以自动生成考勤报表或者实时显示员工打卡情况。 以上只是一个简单的步骤,实际开发过程中需根据具体需求和系统规模进行定制和优化。MATLAB提供了丰富的图像处理和机器学习工具,是开发人脸识别考勤系统的一个很好选择。
hjbvbnvhjhjg
HCIP、软考相关学习PPT提供下载
绿豆BOX UI8版:反编译版六个全新UI+最新后台直播管理源码 最新绿豆BOX反编译版六个UI全新绿豆盒子UI8版本 最新后台支持直播管理 作为UI6的升级版,UI8不仅修复了前一版本中存在的一些BUG,还提供了6套不同的UI界面供用户选择,该版本有以下特色功能: 在线管理TVBOX解析 在线自定义TVBOX 首页布局批量添加会员信息 并支持导出批量生成卡密 并支持导出直播列表管理功能
vue3的一些语法以及知识点
西门子大型Fanuc机器人汽车焊装自动生产线程序经典解析:PLC博图编程与MES系统通讯实战指南,西门子PLC博图汽车焊装自动生产线FANUC机器人程序经典结构解析与MES系统通讯,西门子1500 大型程序fanuc 机器人汽车焊装自动生产线程序 MES 系统通讯 大型程序fanuc机器人汽车焊装自动生产线程序程序经典结构清晰,SCL算法堆栈,梯形图和 SCL混编使用博图 V14以上版本打开 包括: 1、 PLC 博图程序 2 触摸屏程序 ,西门子1500; 大型程序; fanuc机器人; 汽车焊装自动生产线; MES系统通讯; SCL算法; 梯形图; SCL混编; 博图V14以上版本。,西门子博图大型程序:汽车焊装自动生产线MES系统通讯与机器人控制