BeanUtils工具类中的copyProperties方法使用
1、两个包下的BeanUtils.copyProperties对比
BeanUtils是开发中常用到的工具类,而获取这一工具类主要是通过导入org.springframework.beans.BeanUtils或者org.apache.commons.beanutils.BeanUtils包来获取,但是不同的包中BeanUtils的方法使用是不一样的,接下来就对这两个包中的copyProperties方法进行对比。
先来看一下这两个包中的copyProperties方法的定义:
1
2
3
4
|
//org.springframework.beans.BeanUtils
public static void copyProperties(Object source, Object target){....}
//org.apache.commons.beanutils.BeanUtils
public static void copyProperties(Object dest,Object orig){....}
|
由定义可知,在org.springframework.beans.BeanUtils包下的copyProperties第一个参数是被copy的对象,而org.apache.commons.beanutils.BeanUtils中是第二个参数,所以使用时不要弄混。
1)接下来定义两个实体类Student和Teacher,用来模拟目标对象(target/dest)中包含被copy的对象(source/orig)的所有字段时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@Data
public class Student {
private String id;
private String name;
private String age;
public Student(String id, String name, String age) {
this.id = id;
this.name = name;
this.age = age;
}
}
@Data
public class Teacher {
private String id;
private String name;
private String age;
private String sex;
public Teacher() {}
public Teacher(String id, String name, String age, String sex) {
this.id = id;
this.name = name;
this.age = age;
this.sex = sex;
}
}
|
先使用org.springframework.beans.BeanUtils下的copyProperties来进行测试,执行代码如下:
1
2
3
4
5
6
7
8
9
|
import org.springframework.beans.BeanUtils;
public static void main(String[] args) throws Exception {
Student student = new Student(UUID.randomUUID().toString(), "zhangsan", "19");
Teacher teacher = new Teacher();
System.out.println(student.toString());
BeanUtils.copyProperties(student,teacher);
System.out.println(teacher.toString());
}
|
1
2
3
|
执行结果如下:
Student(id=00c47e77-785d-4939-89db-e757979050ec, name=zhangsan, age=19)
Teacher(id=00c47e77-785d-4939-89db-e757979050ec, name=zhangsan, age=19, sex=null)
|
当将引入的包改为org.apache.commons.beanutils.BeanUtils时,将上述中的执行代码进行如下修改:
1
|
BeanUtils.copyProperties(teacher,student); //因为apache包下被copy的参数在第二个位置
|
1
2
3
|
执行结果如下:
Student(id=900d0b93-1913-4022-b86b-127682cd9f5c, name=zhangsan, age=19)
Teacher(id=00c47e77-785d-4939-89db-e757979050ec, name=zhangsan, age=19, sex=null)
|
小结:当目标对象(target/dest)中包含被copy的对象(source/orig)的所有字段时,两种包下的copyProperties方法均可以执行成功。
2)对上述中说到的实体类进行修改,即目标对象(target/dest)中不包含被copy的对象(source/orig)的所有字段时,修改后的内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Data
public class Student {
private String id;
private String name;
private String age;
public Student(String id, String name, String age) {
this.id = id;
this.name = name;
this.age = age;
}
}
@Data
public class Teacher {
private String id;
private String name;
public Teacher() {}
public Teacher(String id, String name) {
this.id = id;
this.name = name;
}
}
|
执行代码与1)中的一样这里就不写了,先使用org.apache.commons.beanutils.BeanUtils中的copyProperties方法,执行结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
Student(id=6f6711ef-fa2f-420d-b0b3-b76998356533, name=zhangsan, age=19)
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/collections/FastHashMap
at org.apache.commons.beanutils.PropertyUtilsBean.getPropertyDescriptor(PropertyUtilsBean.java:964)
at org.apache.commons.beanutils.PropertyUtilsBean.isWriteable(PropertyUtilsBean.java:1479)
at org.apache.commons.beanutils.BeanUtilsBean.copyProperties(BeanUtilsBean.java:280)
at org.apache.commons.beanutils.BeanUtils.copyProperties(BeanUtils.java:135)
at com.learn.controller.BeanController.main(BeanController.java:39)
Caused by: java.lang.ClassNotFoundException: org.apache.commons.collections.FastHashMap
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 5 more
|
接下来使用org.springframework.beans.BeanUtils下的copyProperties方法,执行结果如下:
1
2
|
Student(id=87665c46-9a6f-4936-9c9d-1513e29f2bca, name=zhangsan, age=19)
Teacher(id=87665c46-9a6f-4936-9c9d-1513e29f2bca, name=zhangsan)
|
小结:目标对象(target/dest)中不包含被copy的对象(source/orig)的所有字段时,应选用org.springframework.beans.BeanUtils下的copyProperties方法
3)基于2)中的实体类来看下另外一种情况,修改下执行代码,实体类不变:
1
2
3
4
5
6
7
|
public static void main(String[] args) throws Exception {
Student student = new Student(UUID.randomUUID().toString(), "zhangsan", "19");
Teacher teacher = new Teacher("12334","lisi");
System.out.println(student.toString());
BeanUtils.copyProperties(student,teacher);
System.out.println(teacher.toString());
}
|
先使用org.apache.commons.beanutils.BeanUtils中的copyProperties方法,执行结果如下:
1
2
3
|
Student(id=16bf375a-5925-4411-8bdf-158b3373d261, name=zhangsan, age=19)
Teacher(id=12334, name=zhangsan)
Student(id=12334, name=zhangsan, age=19)
|
接下来使用org.springframework.beans.BeanUtils下的copyProperties方法,执行结果如下:
1
2
3
|
Student(id=2df2fba9-22a2-4cb6-86d7-3d5b47475114, name=zhangsan, age=19)
Teacher(id=12334, name=zhangsan)
Student(id=12334, name=zhangsan, age=19)
|
小结:目标对象(target/dest)中包含被copy的对象(source/orig)的所有字段时,两个包下的copy方法都可以,而且目标对象(target/dest)中多于的对象的值不会被覆盖掉。
总结:
1、org.apache.commons.beanutils.BeanUtils和org.springframework.beans.BeanUtils两个包中的copyProperties方法目标对象和源对象参数的位置是相反,使用时需要注意。
2、使用org.apache.commons.beanutils.BeanUtils进行copy对象时,被copy的对象(source/orig)中包含的字段目标对象(target/dest)必须包含,可以有其他的多于字段,类型可以不相同,但字段名称必须一致;org.springframework.beans.BeanUtils中的没有这个限制。
2、BeanUtils.copyProperties的深浅拷贝问题
2.1、浅拷贝和深拷贝
先来说一下什么是浅拷贝,深拷贝;两者最主要的区别就在于是否是复制了对象的真实实体还是说只是使用了一个指针,两者指向的是内存中的同一个对象。
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址;
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存;同时如果一个类中包含有引用类型变量,则该类拷贝以后引用类型也会被拷贝。
参考博文:
1、深拷贝和浅拷贝的区别
2、java中clone方法的理解(深拷贝、浅拷贝)
3、System.arraycopy()使用原理解析,坑点之:深拷贝和浅拷贝
2.2、BeanUtils.copyProperties深浅拷贝问题
BeanUtils.copyProperties使用起来虽然方便,但是它其实只是浅拷贝,所以类中都是单一的属性,像咱们上边介绍的,可以直接用;如果类中包含有其他的子类就需要考虑下拷贝后目标对象(target/dest)中包含被copy的对象(source/orig)中的子类是否有可能被修改,如果有可能被修改就不能直接使用。
新增加一个实体类Life,接下来我们将上述中的代码进行一个改造:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
@Data
public class Life {
private String life;
public Life(String life) {
this.life = life;
}
}
@Data
public class Teacher {
private String id;
private String name;
private Life life;
public Teacher(String id, String name) {
this.id = id;
this.name = name;
}
public Teacher() {}
}
@Data
public class Student {
private String id;
private String name;
private String age;
private Life life;
public Student(String id, String name, String age) {
this.id = id;
this.name = name;
this.age = age;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import org.springframework.beans.BeanUtils;
public class BeanController {
public static void main(String[] args) throws Exception {
Student student = new Student(UUID.randomUUID().toString(), "zhangsan", "19");
Life life=new Life("young");
student.setLife(life);
Teacher teacher = new Teacher();
System.out.println(student.toString());
BeanUtils.copyProperties(student,teacher);
teacher.getLife().setLife("old");
System.out.println(student.toString());
}
}
|
1
2
3
|
执行结果如下:
Student(id=988cac88-638c-4e52-8421-db39590ed32c, name=zhangsan, age=19, life=Life(life=young))
Student(id=988cac88-638c-4e52-8421-db39590ed32c, name=zhangsan, age=19, life=Life(life=old))
|
正常情况下,Student中的life值应该为young,但是student拷贝给teacher以后,随着teacher中life修改,student中的life也被修改,说明teacher和student两个对象中的life子对象为内存中的同一个life对象,所以BeanUtils.copyProperties属于浅拷贝。
这里介绍一种深拷贝的方法,先将集合转化为字节数组输出流,然后在读取,这样就可以实现深度拷贝,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
public <T> List<T> deepCopy(List<T> src) throws IOException, ClassNotFoundException {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteOut);
out.writeObject(src);
ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
ObjectInputStream in = new ObjectInputStream(byteIn);
@SuppressWarnings("unchecked")
List<T> dest = (List<T>) in.readObject();
return dest;
}
|
参考博文:序列化(Serialization)实现深拷贝
Spring integration学习总结
Spring integration
是一个企业应用集成系统。主要通过消息、通道、消息端点等概念完成不同系统之间的集成。
2.1 消息(Message)
消息包括header
(头)和payload
(负载)
message
的header
一般是id
,时间戳或者目的地址什么的,比如如果你发的消息是个文件,那么header应该存放文件的名称,消息头可以根据需要修改。Payload就是负载,也就睡消息的主要内容,主要通过这个进行通信。
2.2 消息通道(Message channel)
消息传输的通道。分两种,一种是point-to-point
点对点的,一种是publish-subscribe
发布订阅形式的。如果是点对点的channel
,至少会有一个消费者consumer
能收到发送的message
,另一种订阅发布的channel
,spring integration
试图去用广播的形式发布message给那些订阅者subscriber
。
主要实现如下:
PublishSubscribeChannel
:将消息广播给所有的订阅者。
QueueChannel:可以缓存消息,在缓存消息没有达到上限时,消息发送者将消息发送到该通道后立即返回。如果缓存消息数量达到设定的容量,则消息发送者发送消息后会被阻塞,直到消息队列中有空间为止或者超时。对于消息接收者正好相反,尝试获取消息时,如果队列中有消息会立即返回,如果队列中没有消息则会一直阻塞直到超时(需要设定超时时间,不设定的话一直阻塞)。
PriorityChannel
:一个排序队列。默认是根据消息头中的"priority"属性值进行排序,也可以通过实现Comparator<Message<?>>
接口的逻辑来进行排序。
RendezvousChannel
:类似于QueueChannel
,只是容量为0。也就是发送者发送消息时,接受者必须将其接收才会返回,否则一直被阻塞。接受者也是一样。
DirectChannel
:用于点对点场景,但实现的是PublishSubscribeChannel
接口,因此该通道会将消息直接分发给接收者,和PublishSubscribeChannel
的区别就是,接收者只有一个。除此之外,DirectChannel
还有一个最重要的特点就是发送和接收双方处于一个线程当中,如下:发送者发送消息->接收者接收消息并触发处理操作->返回。Spring Integration
中的缺省的通道就是DirectChannel
。例如<int:channel id="xxxx"/>
。
ExecutorChannel
:用于点对点场景,和DirectChannel
的配置相同,但主要的区别是ExecutorChannel
将消息分发的操作委派给一个TaskExecutor
的实例来进行,因此发送消息时不会进行阻塞。
Scoped Channel
:暂不理解。
2.3 消息端点(Message Endpoint)
可以对消息进行处理的地方。包括如下的主要构件:
可以对消息的内容或者结构进行修改,最常见的是修改消息内容的格式,或者对消息头的内容进行添加、修改和删除。用法如下:
1
2
3
|
<int:transformer id="testTransformer" ref="testTransformerBean" input-channel="inChannel"
method="transform" output-channel="outChannel"/>
<beans:bean id="testTransformerBean" class="org.foo.TestTransformer" />
|
决定一个消息是否被过滤掉,只有没有被过滤掉的消息才发送到输出通道上。filter
多用于publish
subscribe
模式中,很多consumer
消费者可以收到相同的message
并且可以用filter
来接收指定类型的消息并将其加工处理。
1
2
3
|
<int:filter input-channel="input" output-channel="output"
ref="exampleObject" method="someBooleanReturningMethod"/>
<bean id="exampleObject" class="example.SomeObject"/>
|
2.3.3 Router
路由器,通常是一个输入通道,多个输出通道。根据消息头或者消息体的内容,决定将消息转发到哪个输出通道上。
1
|
<int:router input-channel="drinks" ref="drinkRouter" method="route"/>
|
2.3.4 Splitter
对应一个输入通道,多个输出通道。Splitter把消息从输入通道上分割发送到它的输出通道上。比如用于把一个复合型的payload
负载分割成很多子负载payloads
,发送到多个输出通道上。
1
|
<int:splitter input-channel="orders" ref="orderSplitter" method="split" output-channel="drinksTransform"/>
|
2.3.5 Aggregator
集合器,和splitter
对应。用于把多种message
组合成一个单一的message
。事实上aggregator
比splitter
要复杂一些,因为它需要维持message的状态,决定什么时候提供组合,什么时候超时timeout
,甚至可以将一个局部的结果放弃,并发送到一个隔离的channel
里。Spring integration
提供了一个CompletionStrategy来配置timeout
超时,是否在超时的时候发送一个结果并且废弃这个channel
(这里应该是说消息废弃这个channel
)。Aggregator
在聚合时,会将消息头中消息ID
相同的消息进行聚合。
1
2
3
|
<int:aggregator input-channel="preparedDrinks" method="prepareDelivery" output-channel="deliveries">
<beans:bean class="com.abc.springintegration.cafe.xml.Waiter"/>
</int:aggregator>
|
2.3.6 Service Activator
用来连接应用的接口和message framework
消息框架的组件,一个输入的频道input message
channel必须被设定,一个service activator
的方法被执行并且返回了一个值,那么可以提供一个输出频道output message channel(如果消息提供自己的返回地址,那么这是可选的)。这个规则适用于所有的consumer endpoints。输入从input channel
到service activator
再到message handler
,然后返回output message
到service activator
到output message channel
。归根结底:被继承组件的主要对外接口。
1
|
<int:service-activator input-channel="coldDrinks" ref="barista" method="prepareColdDrink" output-channel="preparedDrinks"/>
|
2.3.7 Channel Adapter
连接一个消息通道和其他实体之间的对象。channel adapter
也分inbound
内绑定和outbound
外绑定。
Inbound
通道适配:通常的作用是将一个外部系统的资源进行转换,通过消息通道输送到系统中,用于进行后续的处理。
Outbound
通道适配:将系统中的资源通过消息通道发送给Outbound通道适配,然后该“Outbound通道适配”将其转换为外部的资源。
举例:<file:inbound-channel-adapter>
的作用,将文件系统中的文件进行读取,将文件对象或者文件内容发送到消息通道中。
<file:outbound-channel-adapter>
的作用,将处理好的文件资源输出到文件系统中。
Spring Integration
目前支持的常用channel-adapter
如下:
- File
- FTP/FTPS
- HTTP
- JDBC
- SFTP
- Mail
- XML
- 等等
3 使用经验
3.1 重复下载远程服务器上的文件
Spring Integration
下载远程服务器可以使用其工作原理是监控远程服务器上的文件,一旦有新的文件就会首先下载到本地,然后将文件对象装载到channel
中。在测试的过程中发现,这两步是各自独立进行的,也就是说,下载部分只下载远程服务器上存在但本地目录不存在的文件,将文件对象装载到channel
的操作是监控本地目录中的文件,如果没有装载过就进行处理。有时候远程服务器上的相同文件名的文件是定期更新的,需要客户端定期下载,这种情况下的默认处理就有问题了:
1) 下载一次后,如果本地文件已经存在,就不会重复下载。
2) 即使将本地文件进行删除,此时虽然会下载,但下载到本地后不会将文件对象装载到channel中进行后续的处理。
Spring
社区中也没有好的处理办法,这种情况下,可以使用下载到本地后对文件进行改名的方法进行规避,使用方法如下,见红色部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<int-sftp:inbound-channel-adapter
id="sftpInboundAdapter"
session-factory="sftpSessionFactory"
remote-directory="/srv/data/stc/crm/"
local-directory="file:///srv/data/stc/crm/upload"
local-filename-generator-expression="substring(0,lastIndexOf('.'))
+ '_' + T(java.lang.System).currentTimeMillis()
+ substring(lastIndexOf('.'))"
channel="downloadFileChannel"
delete-remote-files="true"
auto-startup="true"
filter="entryListFilter">
<int:poller max-messages-per-poll="1" cron="0/1 * * * * * " />
</int-sftp:inbound-channel-adapter>
|
进行了上面的配置后,下载下来的文件名会变成原来的文件名中加上了时间戳,这样就规避了上述的问题,如果也许需要还原原来的文件名,可以在进入channel
后的后续的处理中再进行处理。
3.2 如何控制inbound-channel-adapter类的对象是否工作
1
2
3
4
|
context = new FileSystemXmlApplicationContext("/conf/stc-job.xml");
SourcePollingChannelAdapter adapter = context.getBean("sftpInboundAdapter", SourcePollingChannelAdapter.class);
adapter.stop(); //停止工作
adapter.start(); //开始工作
|
3.3 Spring Integration 触发 Spring batch 的 job
Spring Integration
的配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
<file:inbound-channel-adapter directory="file:///srv/data/stc/crm"
channel="inputFileChannel" filename-pattern="*.csv">
<int:poller max-messages-per-poll="1" cron="0/1 * * * * * " />
</file:inbound-channel-adapter>
<int:service-activator input-channel="inputFileChannel"
output-channel="jobLaunchRequestChannel"
ref="fileToJobLaunchRequestAdapter"
method="adapt"/>
<int:service-activator input-channel="jobLaunchRequestChannel" output-channel="jobExecutionChannel">
<beans:bean class="org.springframework.batch.integration.launch.JobLaunchingMessageHandler">
<beans:constructor-arg ref="jobLauncher" />
</beans:bean>
</int:service-activator>
<beans:bean id="fileToJobLaunchRequestAdapter" class="com.abc.job.FileToJobLaunchRequestAdapter">
<beans:property name="job" ref="stcJob"/>
</beans:bean>
<!--其中,红色stcJob为Spring batch的job名称,需要做如下的配置:-->
<job id="stcJob">
……
</job>
|
对于Spring Job
的触发类FileToJobLaunchRequestAdapter
,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
public class FileToJobLaunchRequestAdapter implements InitializingBean{
private Job job;
public void setJob(Job job){
this.job = job;
}
public void afterPropertiesSet() throws Exception{
Assert.notNull(job, "A Job must be provided");
}
@ServiceActivator
public JobLaunchRequest adapt(File file) throws NoSuchJobException{
String fileName = file.getAbsolutePath();
if (!fileName.startsWith("/")){
fileName = "/" + fileName;
}
fileName = "file://" + fileName;
String outPutPath = "file:///srv/smartcare/export/";
String outPutFilePath = outPutPath + file.getName();
String outPutTmpFilePath = outPutFilePath;
JobParameters jobParameters = new JobParametersBuilder().addString("input.file.path",
fileName)
.addString("output.file.path", outPutTmpFilePath)
.addString("file.src", outPutTmpFilePath)
.addString("file.original.name", file.getName())
.addString("file.dst", outPutFilePath)
.addLong("time.stamp", System.currentTimeMillis())
.toJobParameters();
//确保job多次执行时,通过让jobParameters发生变化,从而区分job
if (job.getJobParametersIncrementer() != null){
jobParameters = job.getJobParametersIncrementer()
.getNext(jobParameters);
}
return new JobLaunchRequest(job, jobParameters);
}
public void output(File file){
System.out.println("receive file = " + file.getName() + ", path = "
+ file.getPath());
return;
}
}
|
3.4 JAVA 代码中发消息给 Spring Integration 的 channel
Java
代码:
1
2
|
Message<File> message = MessageBuilder.withPayload(file).build();
sftpChannel.send(message);
|
**sftpChannel
**为Spring integration
的channel
名称。
在Spring Integration
的配置文件中,做如下的配置:
1
2
3
4
5
6
|
<int-sftp:outbound-channel-adapter id="sftpOutboundAdapter"
session-factory="sftpSessionFactory"
channel="outFileChannel"
charset="UTF-8"
temporary-file-suffix=".uploading"
remote-directory="/srv"/>
|
其中,**sftpChannel
的名称等于outFileChannel
**就可以了。