0%

置顶 loongshawn 2016-01-11 13:35:15 121579 收藏 97

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

转载请注明来源-作者@loongshawn:http://blog.csdn.net/loongshawn/article/details/50496460

Mybatis

JdbcType

Oracle

MySql

JdbcType

ARRAY

JdbcType

BIGINT

BIGINT

JdbcType

BINARY

JdbcType

BIT

BIT

JdbcType

BLOB

BLOB

BLOB

JdbcType

BOOLEAN

JdbcType

CHAR

CHAR

CHAR

JdbcType

CLOB

CLOB

CLOB–>修改为TEXT

JdbcType

CURSOR

JdbcType

DATE

DATE

DATE

JdbcType

DECIMAL

DECIMAL

DECIMAL

JdbcType

DOUBLE

NUMBER

DOUBLE

JdbcType

FLOAT

FLOAT

FLOAT

JdbcType

INTEGER

INTEGER

INTEGER

JdbcType

LONGVARBINARY

JdbcType

LONGVARCHAR

LONG VARCHAR

JdbcType

NCHAR

NCHAR

JdbcType

NCLOB

NCLOB

JdbcType

NULL

JdbcType

NUMERIC

NUMERIC/NUMBER

NUMERIC/

JdbcType

NVARCHAR

JdbcType

OTHER

JdbcType

REAL

REAL

REAL

JdbcType

SMALLINT

SMALLINT

SMALLINT

JdbcType

STRUCT

JdbcType

TIME

TIME

JdbcType

TIMESTAMP

TIMESTAMP

TIMESTAMP/DATETIME

JdbcType

TINYINT

TINYINT

JdbcType

UNDEFINED

JdbcType

VARBINARY

JdbcType

VARCHAR

VARCHAR

VARCHAR


注意到, MyBatis的JdbcType中部分没有对应到Oracle和Mysql的数据类型中(或许由于自己遗漏),不过不用担心,后续大家碰到再具体分析;同时上述对应关系不一定是一一对应,请大家了解。

大家主要掌握基本的数字、时间、字符串就足以应对日常开发了。

Mybatis JdbcType官方文档

Mybatis JdbcType官方文档

查阅Mybatis JdbcType官方文档是很有必要的!

对于自己不肯定的,调整代码多尝试下,能够使自己加深印象!

  • 2017-04-26 修改内容:MySQL中没有CLOB类型,谢谢@火灵 指正。

Mybatis

JdbcType

Oracle

MySql

JdbcType

CLOB

CLOB

CLOB–>修改为TEXT


1、什么是MyBatis?

  MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。

  iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL Maps和Data Access Objects(DAO)。

  MyBatis 是支持普通 SQL查询,存储过程和高级映射的优秀持久层框架。MyBatis 消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis 使用简单的 XML或注解用于配置和原始映射,将接口和 Java 的POJOs(Plain Ordinary Java Objects,普通的 Java对象)映射成数据库中的记录。

PS:本文所有代码下载链接:http://pan.baidu.com/s/1gf8CPQN 密码:t2x9 

  再给大家推荐一个比较好的mybatis学习网站:www.mybatis.cn

2、MyBatis 入门实例基于xml配置

  ①、创建MySQL数据库:mybatisDemo和表:person

1

2

3

create database mybatisDemo;

use mybatisDemo;

create table person(pid int primary key AUTO_INCREMENT, pname varchar(``50``), page int``);

  

  ②、建立一个Java工程,并导入相应的jar包

   

  相应的 jar 包下载链接:http://pan.baidu.com/s/1skZM09Z  密码:nkt6

   ③、在 MyBatisDemo 工程中添加数据库配置文件 mybatis-configuration.xml

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

<?xml version=``"1.0" encoding=``"UTF-8"``?>

<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"``>

<configuration>

<!-- 可以配置多个运行环境,但是每个 SqlSessionFactory 实例只能选择一个运行环境

一、development:开发模式

二、work:工作模式-->

<environments default``=``"development"``>

<!--id属性必须和上面的``default``一样  -->

<environment id=``"development"``>

<!--事务管理器

一、JDBC:这个配置直接简单使用了 JDBC 的提交和回滚设置。它依赖于从数据源得到的连接来管理事务范围

二、MANAGED:这个配置几乎没做什么。它从来不提交或回滚一个连接。而它会让容器来管理事务的整个生命周期

比如 spring 或 JEE 应用服务器的上下文,默认情况下,它会关闭连接。然而一些容器并不希望这样,

因此如果你需要从连接中停止它,就可以将 closeConnection 属性设置为 false``,比如:

<transactionManager type=``"MANAGED"``>

<property name=``"closeConnection" value=``"false"``/>

</transactionManager>

-->

<transactionManager type=``"JDBC"``/>

<!--dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象源  -->

<dataSource type=``"POOLED"``>

<property name=``"driver" value=``"com.mysql.jdbc.Driver"``/>

<property name=``"url" value=``"jdbc:mysql://localhost:3306/mybatisdemo"``/>

<property name=``"username" value=``"root"``/>

<property name=``"password" value=``"root"``/>

</dataSource>

</environment>

</environments>

</configuration>

  ④、定义表所对应的实体类

  

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

package com.ys.bean;

public class Person {

private int pid;

private String pname;

private int page;

public int getPid() {

return pid;

}

public void setPid(``int pid) {

this``.pid = pid;

}

public String getPname() {

return pname;

}

public void setPname(String pname) {

this``.pname = pname;

}

public int getPage() {

return page;

}

public void setPage(``int page) {

this``.page = page;

}

@Override

public String toString() {

return "Person [pid=" + pid + ", pname=" + pname + ", page=" + page

+ "]"``;

}

}

  ⑤、定义操作 person 表的sql映射文件personMapper.xml

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

<?xml version=``"1.0" encoding=``"UTF-8" ?>

<!DOCTYPE mapper

PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"

"http://mybatis.org/dtd/mybatis-3-mapper.dtd"``>

<mapper namespace=``"com.ys.bean.personMapper"``>

<!-- 根据 pid 查询 person 表中的数据

id:唯一标识符,此文件中的id值不能重复

resultType:返回值类型,一条数据库记录也就对应实体类的一个对象

parameterType:参数类型,也就是查询条件的类型

-->

<select id=``"selectPersonById"

resultType=``"com.ys.bean.Person" parameterType=``"int"``>

<!-- 这里和普通的sql 查询语句差不多,对于只有一个查询条件后面的 #{pid}表示占位符,里面不一定要写pid,写啥都可以,但是不要空着;如果有多个查询条件,则要写pojo类里面的属性 -->

select * from person where pid = #{pid}

</select>

<!-- 查询person 表所有数据 -->

<select id=``"getAllPerson" resultType=``"com.ys.bean.Person"``>

select * from person

</select>

<!-- 根据id更新数据 -->

<update id=``"updatePersonById" parameterType=``"com.ys.bean.Person"``>

update person set pname=#{pname},page=#{page} where pid = #{pid}

</update>

<!-- 向 person 表插入一条数据 -->

<insert id=``"addPerson" parameterType=``"com.ys.bean.Person"``>

insert into person(pid,pname,page) values(#{pid},#{pname},#{page})

</insert>

<!-- 根据 pid 删除数据 -->

<delete id=``"deletePersonById" parameterType=``"Long"``>

delete from person where pid=#{pid}

</delete>

</mapper>

  ⑥、向 mybatis-configuration.xml 配置文件中注册 personMapper.xml 文件

1

2

3

4

5

<mappers>

<!-- 注册personMapper.xml文件,

personMapper.xml位于com.ys.bean这个包下,所以resource写成com/ys/bean/personMapper.xml-->

<mapper resource=``"com/ys/bean/personMapper.xml"``/>

</mappers>

  如下图所示:

   ⑦、创建测试类

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

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

package com.ys.test;

import java.io.InputStream;

import java.util.List;

import org.apache.ibatis.session.SqlSession;

import org.apache.ibatis.session.SqlSessionFactory;

import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import org.junit.Before;

import org.junit.Test;

import com.ys.bean.Person;

public class MyBatisTest {

SqlSession session;

@Before

public void beforeLoadXML(){

InputStream inputStream = MyBatisTest.``class``.

getClassLoader().getResourceAsStream(``"mybatis-configuration.xml"``);

SqlSessionFactory sqlSessionFactory =

new SqlSessionFactoryBuilder().build(inputStream);

session = sqlSessionFactory.openSession();

}

@Test

public void testSelectById(){

String statement = "com.ys.bean.personMapper"``+``".selectPersonById"``;

Person p = session.selectOne(statement, 1``);

System.out.println(p);

session.close();

}

@Test

public void testGetAllPerson(){

String statement = "com.ys.bean.personMapper.getAllPerson"``;

List<Person> listPerson = session.selectList(statement);

System.out.println(listPerson);

session.close();

}

@Test

public void updateById(){

String statement = "com.ys.bean.personMapper.updatePersonById"``;

Person p = new Person();

p.setPid(``1``);

p.setPname(``"aaa"``);

p.setPage(``11``);

session.update(statement, p);

session.commit();

session.close();

}

@Test

public void addPerson(){

String statement = "com.ys.bean.personMapper.addPerson"``;

Person p = new Person();

p.setPname(``"add"``);

p.setPage(``11``);

session.insert(statement, p);

session.commit();

session.close();

}

@Test

public void deletePersonById(){

String statement = "com.ys.bean.personMapper.deletePersonById"``;

session.delete(statement, 1``);

session.commit();

session.close();

}

}

3、MyBatis 入门实例注解配置

   ①、上面的前面四步都是一样的,但是第五步不一样,我们不需要创建 personMapper.xml 文件,首先在 src 目录下创建 personMapper.java 文件

   内容如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

package com.ys.annocation;

import org.apache.ibatis.annotations.Delete;

import org.apache.ibatis.annotations.Insert;

import org.apache.ibatis.annotations.Select;

import org.apache.ibatis.annotations.Update;

import com.ys.bean.Person;

public interface PersonMapper {

@Insert``(``"insert into person(pid,pname,page) values(#{pid},#{pname},#{page})"``)

public int add(Person person);

@Select``(``"select * from person where pid = #{pid}"``)

public Person getPerson(``int pid);

@Update``(``"update person set pname=#{pname},page=#{page} where pid = #{pid}"``)

public int updatePerson(Person preson);

@Delete``(``"delete from person where pid=#{pid}"``)

public int deletePerson(``int pid);

}

  ②、向 mybatis-configuration.xml 配置文件中注册 personMapper.xml 文件

   ③、编写测试类

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

@Test

public void testAnnocation(){

PersonMapper mapper = session.getMapper(PersonMapper.``class``);

Person p = new Person();

p.setPid(``7``);

p.setPname(``"abc"``);

p.setPage(``11``);

mapper.add(p);

Person p1 = mapper.getPerson(``3``);

System.out.println(p1);

p.setPage(``100``);

mapper.updatePerson(p);

mapper.deletePerson(``7``);

session.commit();

session.close();

}

4、MyBatis 入门实例  一对一  基于xml配置

  这里我们以老师和班级为例,假设一般班级只能拥有有一个老师,一个老师只能带一个班级。

  ①、创建实体类

  

  Teacher.java

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

package one.to.one;

public class Teacher {

private int tid;

private String tname;

private Classes classes;

public int getTid() {

return tid;

}

public void setTid(``int tid) {

this``.tid = tid;

}

public String getTname() {

return tname;

}

public void setTname(String tname) {

this``.tname = tname;

}

public Classes getClasses() {

return classes;

}

public void setClasses(Classes classes) {

this``.classes = classes;

}

@Override

public String toString() {

return "Teacher [tid=" + tid + ", tname=" + tname + ", classes=" + classes + "]"``;

}

}

  Classes.java

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

package one.to.one;

public class Classes {

private int cid;

private String cname;

private Teacher teacher;

public int getCid() {

return cid;

}

public void setCid(``int cid) {

this``.cid = cid;

}

public String getCname() {

return cname;

}

public void setCname(String cname) {

this``.cname = cname;

}

public Teacher getTeacher() {

return teacher;

}

public void setTeacher(Teacher teacher) {

this``.teacher = teacher;

}

@Override

public String toString() {

return "Classes [cid=" + cid + ", cname=" + cname + ", teacher=" + teacher + "]"``;

}

}

  ②、在数据库中根据实体类创建相应的数据表

  ③、定义操作 Classes 表的sql映射文件classesMapper.xml 

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

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

<?xml version=``"1.0" encoding=``"UTF-8" ?>

<!DOCTYPE mapper

PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"

"http://mybatis.org/dtd/mybatis-3-mapper.dtd"``>

<mapper namespace=``"one.to.one.classesMapper"``>

<!--

方式一:嵌套结果:使用嵌套结果映射来处理重复的联合结果的子集

封装联表查询的数据(去除重复的数据)

select * from classes c, teacher t where c.tid=t.tid and c.tid=#{tid}

-->

<select id=``"getClasses" resultMap=``"getClassesMap" parameterType=``"int"``>

select * from classes c ,teacher t

where c.tid=t.tid and c.tid=#{tid}

</select>

<resultMap type=``"one.to.one.Classes" id=``"getClassesMap"``>

<id column=``"cid" property=``"cid"``/>

<result column=``"cname" property=``"cname"``/>

<association property=``"teacher" javaType=``"one.to.one.Teacher"``>

<id column=``"tid" property=``"tid"``></id>

<result column=``"tname" property=``"tname"``/>

</association>

</resultMap>

<!--

方式一:嵌套结果:使用嵌套结果映射来处理重复的联合结果的子集

封装联表查询的数据(去除重复的数据)

select * from teacher t,classes c where t.cid = c.cid and t.cid=#{cid}

-->

<select id=``"getTeacher" resultMap=``"getTeacherMap" parameterType=``"int"``>

select * from teacher t,classes c

where t.cid = c.cid and t.cid=#{cid}

</select>

<resultMap type=``"one.to.one.Teacher" id=``"getTeacherMap"``>

<id column=``"tid" property=``"tid"``/>

<result column=``"tname" property=``"tname"``/>

<association property=``"classes" javaType=``"one.to.one.Classes"``>

<id column=``"cid" property=``"cid"``/>

<result column=``"cname" property=``"cname"``/>

</association>

</resultMap>

<!--

方式二:嵌套查询:通过执行另外一个SQL映射语句来返回预期的复杂类型

SELECT * FROM classes WHERE cid=``1``;

SELECT * FROM teacher WHERE tid=``1  

property:别名(属性名)    column:列名 -->

<!-- 把teacher的字段设置进去 -->

<select id=``"getClasses2" resultMap=``"getClassesMap2"``>

select * from classes c where c.cid = #{cid}

</select>

<resultMap type=``"one.to.one.Classes" id=``"getClassesMap2"``>

<id column=``"cid" property=``"cid"``/>

<result column=``"cname" property=``"cname"``/>

<collection property=``"teacher" column=``"tid" select=``"getTeacherCollection"``>

</collection>

</resultMap>

<select id=``"getTeacherCollection" resultType=``"one.to.one.Teacher"``>

select tid tid,tname tname from teacher where tid=#{tid}

</select>

</mapper>

  说明:我们这里一对一的关联操作,有两种方式:

    1、使用嵌套结果映射来处理重复的联合结果的子集

    2、通过执行另外一个SQL映射语句来返回预期的复杂类型

    相关属性解释:

    

  ④、向 mybatis-configuration.xml 配置文件中注册 classesMapper.xml 文件

  

  ⑤、编写测试类

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

42

43

44

45

46

47

48

49

50

51

52

53

54

package one.to.one;

import java.io.InputStream;

import org.apache.ibatis.session.SqlSession;

import org.apache.ibatis.session.SqlSessionFactory;

import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import org.junit.Before;

import org.junit.Test;

import com.ys.test.MyBatisTest;

public class OneToOneTest {

SqlSession session;

@Before

public void beforeLoadXML(){

InputStream inputStream = MyBatisTest.``class``.

getClassLoader().getResourceAsStream(``"mybatis-configuration.xml"``);

SqlSessionFactory sqlSessionFactory =

new SqlSessionFactoryBuilder().build(inputStream);

session = sqlSessionFactory.openSession();

}

@Test

public void testGetClasses(){

String statement = "one.to.one.classesMapper.getClasses"``;

Classes c = session.selectOne(statement, 1``);

System.out.println(c);

}

@Test

public void testGetTeacher(){

String statement = "one.to.one.classesMapper.getTeacher"``;

Teacher t = session.selectOne(statement, 1``);

System.out.println(t);

}

@Test

public void testGetClasses2(){

String statement = "one.to.one.classesMapper.getClasses2"``;

Classes c = session.selectOne(statement, 1``);

System.out.println(c);

}

}

4、MyBatis 入门实例  一对多,多对一  基于xml配置

  这里我们以班级和学生为例,一个班级里面对应多个学生,这是一对多;反过来,多个学生对应一个班级,这是多对一

  ①、建立学生和班级的实体类

  Student.java

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

package one.to.many;

public class Student {

private int sid;

private String sname;

private Classes classes;

public int getSid() {

return sid;

}

public void setSid(``int sid) {

this``.sid = sid;

}

public String getSname() {

return sname;

}

public void setSname(String sname) {

this``.sname = sname;

}

public Classes getClasses() {

return classes;

}

public void setClasses(Classes classes) {

this``.classes = classes;

}

@Override

public String toString() {

return "Student [sid=" + sid + ", sname=" + sname + ", classes=" + classes + "]"``;

}

}

    Classes.java

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

package one.to.many;

import java.util.Set;

public class Classes {

private int cid;

private String cname;

private Set<Student> students;

public int getCid() {

return cid;

}

public void setCid(``int cid) {

this``.cid = cid;

}

public String getCname() {

return cname;

}

public void setCname(String cname) {

this``.cname = cname;

}

public Set<Student> getStudents() {

return students;

}

public void setStudents(Set<Student> students) {

this``.students = students;

}

@Override

public String toString() {

return "Classes [cid=" + cid + ", cname=" + cname + ", students=" + students + "]"``;

}

}

②、在数据库中根据实体类创建相应的数据表

  ③、多对一:定义操作 Classes 表的sql映射文件classesMapper.xml 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

<?xml version=``"1.0" encoding=``"UTF-8" ?>

<!DOCTYPE mapper

PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"

"http://mybatis.org/dtd/mybatis-3-mapper.dtd"``>

<mapper namespace=``"one.to.many.classesMapper"``>

<select id=``"getClasses" resultMap=``"getClassesMap"``>

select * from classes c,student s where s.cid=c.cid and c.cid=#{cid}

</select>

<resultMap type=``"one.to.many.Classes" id=``"getClassesMap"``>

<id column=``"cid" property=``"cid"``></id>

<result column=``"cname" property=``"cname"``/>

<collection property=``"students" ofType=``"one.to.many.Student"``>

<id column=``"sid" property=``"sid"``/>

<result column=``"sname" property=``"sname"``/>

</collection>

</resultMap>

</mapper>

  ④、一对多:定义操作 Student 表的sql映射文件studentMapper.xml

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

<?xml version=``"1.0" encoding=``"UTF-8" ?>

<!DOCTYPE mapper

PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"

"http://mybatis.org/dtd/mybatis-3-mapper.dtd"``>

<mapper namespace=``"many.to.one.studentMapper"``>

<select id=``"getStudents" resultMap=``"getStudentMap"``>

select * from classes c,student s where s.cid=c.cid and s.sid=#{sid}

</select>

<resultMap type=``"one.to.many.Student" id=``"getStudentMap"``>

<id column=``"sid" property=``"sid"``></id>

<result column=``"sname" property=``"sname"``/>

<association property=``"classes" javaType=``"one.to.many.Classes"``>

<id column=``"cid" property=``"cid"``/>

<result column=``"cname" property=``"cname"``/>

</association>

</resultMap>

</mapper>

  ⑤、向 mybatis-configuration.xml 配置文件中注册 classesMapper.xml 、studentMapper.xml文件

⑥、编写测试类

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

42

43

44

45

46

47

48

49

package one.to.many;

import java.io.InputStream;

import org.apache.ibatis.session.SqlSession;

import org.apache.ibatis.session.SqlSessionFactory;

import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import org.junit.Before;

import org.junit.Test;

import com.ys.test.MyBatisTest;

public class OneToManyTest {

SqlSession session;

@Before

public void beforeLoadXML(){

InputStream inputStream = MyBatisTest.``class``.

getClassLoader().getResourceAsStream(``"mybatis-configuration.xml"``);

SqlSessionFactory sqlSessionFactory =

new SqlSessionFactoryBuilder().build(inputStream);

session = sqlSessionFactory.openSession();

}

@Test

public void testGetClasses(){

String statement = "one.to.many.classesMapper.getClasses"``;

Classes c = session.selectOne(statement, 1``);

System.out.println(c);

System.out.println(c.getStudents().size());

}

@Test

public void testGetStudents(){

String statement = "many.to.one.studentMapper.getStudents"``;

Student s = session.selectOne(statement, 1``);

System.out.println(s);

System.out.println(s.getClasses());

}

}

5、MyBatis 入门实例  多对多  基于xml配置

  这里我们以 users 表和 groups 表为例,一个 users 可能加入多个 groups,而一个 groups 可能包含多个 users,故构成 多对多 的关联

  ①、在数据库中建立相应的表

  users 表

  

  groups 表

  

  两者之间的关联表users_groups表

  

  ②、建立对应的实体类

  Users.java

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

package many.to.many;

import java.util.Set;

public class Users {

private int uid;

private String uname;

private Set<Groups> groups;

public int getUid() {

return uid;

}

public void setUid(``int uid) {

this``.uid = uid;

}

public String getUname() {

return uname;

}

public void setUname(String uname) {

this``.uname = uname;

}

public Set<Groups> getGroups() {

return groups;

}

public void setGroups(Set<Groups> groups) {

this``.groups = groups;

}

@Override

public String toString() {

return "User [uid=" + uid + ", uname=" + uname + ", groups=" + groups + "]"``;

}

}

    Groups.java

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

package many.to.many;

import java.util.Set;

public class Groups {

private int gid;

private String gname;

private Set<Users> users;

public int getGid() {

return gid;

}

public void setGid(``int gid) {

this``.gid = gid;

}

public String getGname() {

return gname;

}

public void setGname(String gname) {

this``.gname = gname;

}

public Set<Users> getUsers() {

return users;

}

public void setUsers(Set<Users> users) {

this``.users = users;

}

@Override

public String toString() {

return "Group [gid=" + gid + ", gname=" + gname + ", users=" + users + "]"``;

}

}

    Users_Groups.java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

package many.to.many;

public class Users_Groups {

private Users user;

private Groups group;

public Users getUser() {

return user;

}

public void setUser(Users user) {

this``.user = user;

}

public Groups getGroup() {

return group;

}

public void setGroup(Groups group) {

this``.group = group;

}

}

  ③、多对多:定义操作 sql映射文件userMapper.xml

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

<?xml version=``"1.0" encoding=``"UTF-8" ?>

<!DOCTYPE mapper

PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"

"http://mybatis.org/dtd/mybatis-3-mapper.dtd"``>

<mapper namespace=``"many.to.many.userMapper"``>

<!-- 给一个用户 id,查看该用户下的所有用户组信息 -->

<select id=``"getUsers" resultMap=``"getGroupMap"``>

select g.gid,g.gname from users_groups ug,groups g

where ug.group_id=g.gid and ug.user_id=#{uid}

</select>

<resultMap type=``"many.to.many.Groups" id=``"getGroupMap"``>

<id column=``"gid" property=``"gid"``/>

<result column=``"gname" property=``"gname"``/>

<collection property=``"users" ofType=``"many.to.many.Users"``>

<id column=``"uid" property=``"uid"``/>

<result column=``"uname" property=``"uname"``/>

</collection>

</resultMap>

</mapper>

⑤、向 mybatis-configuration.xml 配置文件中注册 userMapper.xml文件

⑥、编写测试类

1

2

3

4

5

6

7

8

9

@Test

public void testGetGroups(){

String statement = "many.to.many.userMapper.getUsers"``;

List<Groups> listGroup = session.selectList(statement,``1``);

for``(Groups g : listGroup){

System.out.println(g.toString());

}

}

  在《上篇》中,基本的项目结构已经搭建起来了,但是有个问题,层与层之间虽然使用了接口进行隔离,但实例化接口的时候,还引入了接口实现类的依赖。如下图:

  面向接口编程,Controller应该只依赖于站点业务层的接口,而不能依赖于具体的实现,否则,就违背了在层之间设置接口的初衷了。

  另外,如果上层只依赖于下层的接口,在做单元测试的时候,就可以用MoqFakes等Mock工具来按实际需求来模拟接口的实现,就可以灵活的控制接口的返回值来对各种情况进行测试,如果依赖于具体的实现,项目的可测试性将大大减小,不利于进行自动化的单元测试。

  要不依赖于具体的实现,就不能使用通常的 T t = new T() 的方式来获得一个类的实例了,需要通过IOC容器来对对象生命周期,依赖关系等进行统一的管理。

  .net中可用的IOC容器非常多,如 CastleWindsor,Unity,Autofac,ObjectBuilder,StructureMap,Spring.Net等,这些第三方工具各不相同,但功能大体都相同,大都需要事先对接口与实现进行配对(通过代码或配置文件),然后由系统自动或手动来通过接口来获得相应实现类的实例,对象实例化的工作由IOC容器自动完成。

  MEF相对于上面的这些IOC容器有什么优势呢?下面是我推荐的理由:

  1. .net4.0 自带:MEF的功能在 System.ComponentModel.Composition.dll 程序集中,直接引用即可使用,不用安装第三方组件
  2. 0 配置:MEF是不需要使用配置文件或代码对接口与实现进行一一配对的,只需要简单的使用几个Attribute特性,就能自动完成源与目标的配对工作
  3. 自动化:系统初始化时自动遍历程序目录或指定文件夹下的dll,根据程序集中接口与类的特定Attribute特性进行自动配对。

在桌面程序中,需要完成两个部分的目录匹配,一个是dll中的匹配,另一个为exe程序集中的匹配,分别使用到DirectoryCatalog与AssemblyCatalog两个目录类。而两个目录类需加入到 AggregateCatalog 目录类中,才能参与组合容器CompositionContainer的初始化。

  在服务提供方的实现类中,使用 ExportAttribute 标记要与之匹配的接口,如下图所示。在服务调用方,使用 ImportAttribute 来给接口注入实现类的实例,如上图所示。

  由于调用方法为静态的方法,Program类的实例仍需手动从组件容器中获得,然后尝试登录:

  输出结果,接口AccountContract并没有赋值,但能输出其实现类的信息,同时登录也能成功调用:

  在MVC的项目中,IOC组件是通过 DependencyResolver类中的 SetResolver(IDependencyResolver resolver) 方法来向MVC提供注册点的,所以我们只需要实现一个 IDependencyResolver 接口的MEF实现类,即可完成MEF在MVC中的注册工作。

  另外考虑到Web应用程序的无状态性,即每次访问都是独立进行的,所以IOC组件产生的对象实例也必须唯一,否则不同用户的操作就可能串线,产生相互干扰。在这里,我们使用HttpContext.Current.Items集合来保存 组合容器CompositionContainer的实例,以使每个用户的数据保持独立,并且同一用户的同一个Http请求中使用同一对象实例。另外考虑到可能会有某种情况下需要手动获取组合容器中的实例,把组合容器缓存到了当前上下文中的Application中。

  MefDependencySolver实现代码如下: 

复制代码

1 ///


2 /// MEF依赖关系解析类 3 ///

4 public class MefDependencySolver : IDependencyResolver 5 {
6 private readonly ComposablePartCatalog _catalog; 7 private const string HttpContextKey = “MefContainerKey”;
8
9 public MefDependencySolver(ComposablePartCatalog catalog) 10 { 11 _catalog = catalog; 12 } 13
14 public CompositionContainer Container 15 { 16 get
17 { 18 if (!HttpContext.Current.Items.Contains(HttpContextKey)) 19 { 20 HttpContext.Current.Items.Add(HttpContextKey, new CompositionContainer(_catalog)); 21 } 22 CompositionContainer container = (CompositionContainer)HttpContext.Current.Items[HttpContextKey]; 23 HttpContext.Current.Application[“Container”] = container; 24 return container; 25 } 26 } 27
28 #region IDependencyResolver Members
29
30 public object GetService(Type serviceType) 31 { 32 string contractName = AttributedModelServices.GetContractName(serviceType); 33 return Container.GetExportedValueOrDefault<object>(contractName); 34 } 35
36 public IEnumerable<object> GetServices(Type serviceType) 37 { 38 return Container.GetExportedValues<object>(serviceType.FullName); 39 } 40
41 #endregion
42 }

复制代码

   在Global.asax.cs的Application_Start方法中初始化MEF容器,由于Web应用程序中只需要在DLL中查找匹配,所以只使用DirectoryCatalog即可。

   在AccountController类中加入MEF的Attribute标签,并删除原来构造函数中的AccountContract属性的赋值代码

  在加入了IOC组件之后,我们的架构就变成了面向接口编程的架构了,上层代码仅依赖于下层的接口,而不依赖于下层的具体实现,为了防止站点业务层的实现代码中出现如下所示的代码:

IAccountSiteContract accountContract = new AccountSiteService();

上一篇文章中也提到过,需要把站点业务层中的实现类的可访问性由 public 修改为 Internal,以实现上层代码对下层代码的真正隔离。

  其他代码不变,运行网站,同样能正常调用登录功能

  最后,给个温馨提示:

  MEF的导出导入是整体关联的,只要树中某一个部件匹配失败,整个树将无法实例化,也就是会出现Import的属性值为null的情况,这种情况下,可以使用MEF开发团队提供的调试工具MEFX来进行问题的快速定位。具体使用方法参考如下:

   GMFrameworkForBlog2.zip

  为了让大家能第一时间获取到本架构的最新代码,也为了方便我对代码的管理,本系列的源码已加入微软的开源项目网站 http://www.codeplex.com,地址为:

  https://gmframework.codeplex.com/

  可以通过下列途径获取到最新代码:

  • 如果你是本项目的参与者,可以通过VS自带的团队TFS直接连接到 https://tfs.codeplex.com:443/tfs/TFS17 获取最新代码

  • 如果你安装有SVN客户端(亲测TortoiseSVN 1.6.7可用),可以连接到 https://gmframework.svn.codeplex.com/svn 获取最新代码

  • 如果以上条件都不满足,你可以进入页面 https://gmframework.codeplex.com/SourceControl/latest 查看最新代码,也可以点击页面上的 Download 链接进行压缩包的下载,你还可以点击页面上的 History 链接获取到历史版本的源代码

  • 如果你想和大家一起学习MVC,学习EF,欢迎加入Q群:5008599(群发言仅限技术讨论,拒绝闲聊,拒绝酱油,拒绝广告)

  • 如果你想与我共同来完成这个开源项目,可以随时联系我。

  1. MVC实用架构设计(〇)——总体设计
  2. MVC实用架构设计(一)——项目结构搭建
  3. MVC实用架构设计(二)——使用MEF应用IOC
  4. MVC实用架构设计(三)——EF-Code First(1):Repository,UnitOfWork,DbContext
  5. MVC实用架构设计(三)——EF-Code First(2):实体映射、数据迁移,重构
  6. MVC实用架构设计(三)——EF-Code First(3):使用T4模板生成相似代码
  7. MVC实用架构设计(三)——EF-Code First(4):数据查询
  8. MVC实用架构设计(三)——EF-Code First(5):二级缓存
  9. MVC实体架构设计(三)——EF-Code First(6):数据更新
  10. 未完待续。。。

在WPF里使用MVVM开发的时候,似乎总是不可避免的会遇到这样一个问题:ViewModel在处理完业务之后需要关闭这个Window,这时候要怎么处理?

网上有很多解决方案:有的在ViewModel抛出一个事件,在View端使用(XXXViewModel)this.DataContext的方式去响应事件;有的通过Trigger、Behavior、Action之类的方式曲线救国;还有的使用了其他的第三方框架。

这些操作从某个层面上来说确实能实现这个功能,但是有的操作起来过于麻烦,有的实现功能了但是大大的违反了MVVM的原则,有的则有很多局限性(比如只能针对关闭了Window之后什么都不做,或者必须要求Window有无参的构造函数)。直到我发现了还可以有这样一种操作之后,我觉得这应该处理这个问题的最佳实践了:优雅,简洁,符合MVVM的思想还没有局限性。

在MVVM里,View和ViewModel之间通过绑定完成了大部分的操作,这也是MVVM最为推荐的做法。那么,为什么View的关闭这个事情不能通过绑定来实现呢?是因为Window没有控制关闭这个操作的属性么?不,在没有使用MVVM,直接在后台写代码创建了一个Window的时候,我们只需要将这个Window的DialogResult属性赋值(不管是true还是false)就可以将这个窗口关闭。那么我们为什么不直接将Window的DialogResult属性在ViewModel绑定呢?

秉着这样的思想我去做了这个实验,编译通过,运行的时候得到了这样的异常提示:

“不能在“ChildWindow”类型的“DialogResult”属性上设置“Binding”。只能在 DependencyObject 的 DependencyProperty 上设置“Binding”。

这个提示已经很明显了:为什么不能直接对Window的DialogResult做绑定,因为DialogResult这个属性不是依赖属性,WPF里面所有的绑定都必须只能绑定依赖属性,而WPF里绝大部分的属性都是依赖属性,但是DialogResult恰恰不是依赖属性,所以不能绑定。

此路不通之后就有了上面的各种解决方法,但是为什么不这样想:DialogResult不是依赖属性,那我注册一个依赖属性不就完了么?WPF又不是不让注册。

注册依赖属性代码如下:

复制代码

public static class DialogCloser
{ public static readonly DependencyProperty DialogResultProperty = DependencyProperty.RegisterAttached( “DialogResult”, typeof(bool?), typeof(DialogCloser), new PropertyMetadata(DialogResultChanged)); private static void DialogResultChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{ var window = d as Window; if (window != null)
{
window.DialogResult = e.NewValue as bool?;
}
} public static void SetDialogResult(Window target, bool? value)
{
target.SetValue(DialogResultProperty, value);
}
}

复制代码

然后在View端绑定这个依赖属性DialogResult:

<Window x:Class=“mvvm_demo_close_window.ChildWindow” …
xmlns:xc=“clr-namespace:mvvm_demo_close_window” xc:DialogCloser.DialogResult=“{Binding DialogResult}”>

然后在ViewModel端将这个当做正常的依赖属性去操作就行了,当this.DialogResult=true的时候就自动在ViewModel关闭了子窗口:

复制代码

public class ChildWindowViewModel : ViewModelBase
{ private bool? dialogResult; public bool? DialogResult
{ get { return this.dialogResult; } set { this.dialogResult = value;
RaisePropertyChanged(“DialogResult”);
}
} //用来接收关闭按钮的Command
public ICommand CloseCmd
{ get { return new DelegateCommand((obj) => { this.DialogResult = true;
});
}
}
}

复制代码

这是我目前发现的最优雅的解决方案,DialogCloser也完全可以复用,如果大家还有更好的方案,欢迎提出来一起讨论。源代码已在下方给出,需要的自行下载。

点击下载源代码

前面介绍了WPF的基本概念和一些相关知识,我们了解到开发WPF应用程序可以使用现成的框架和模式,最为合适的莫过于时下正热的MVVM模式,所以这里我们也列出针对MVVM模式的已有开源框架:

上面除了WPFToolKit和MEF之外都是一些常用的MVVM框架,连带自己开发的一共是八个,所以自己也取了一个响亮的名字——MVVM 八大框架!圣殿骑士本人实际项目中只用到了三个框架,之前WPF使用过MVVMHelpers,在Silverlight项目当中用过MVVMLight+MEF,后来就一直使用自己开发的框架,其他框架也研究了很长时间,但都是为了开发MVVM框架借鉴之用。框架虽然众多,但万变不离其宗,通用功能如下:

1,Model、View 和 ViewModel之间的关系:View和ViewModel如何关联起来(IOC)以及如何通信(通常采用Message),ViewModel和Model之间的弱关联(通常采用接口或者简单注入)。

2,事件驱动模式在MVVM模式采用Command和Attached Behaviors的形式。

3,属性及ViewModel的NotifyPropertyChanged处理。

4,提供完整的单元测试,这也是保证框架的稳定性和维护性的保证。

  也许有一些朋友会问我为什么要研究这么多框架,其实做项目只需要认真研究并实践其中一个就行,研究它们的原因则主要归功于自己开发MVVM框架的需要。下面就让我们来看一下具体有哪些MVVM的开源框架(具体到每一个框架后面有时间再阐述,一篇文章写得确实很累):

2,最强大且功能最多的组合框架——Prism

下载地址:http://compositewpf.codeplex.com/

开发者:微软patterns & practices团队

是否支持Silverlight:是

学习资料:Composite Application Guidance for WPF and Silverlight - May 2010.chm以及Quickstarts和StockTraderRI等。

源码截图如下:

2010-10-6 23-54-02 

                                             图4

  Prism——之前又叫Composite Application Guidance for WPF and Silverlight,它是构建复杂的基于WPF/Silverlight企业级应用的主流框架。Prism中有几个比较重要的概念:

1,BootStrapper:应用程序切入点,继承Prism的UnityContainer或MEF提供的MefUnityContainer,为系统提供一个容器。

2,Shell:它是一个外壳,通过UI元素和Region布局页面。 它是应用程序的顶级窗口,显示的内容一般就由View来填充,Shell本身并不知道它包含了哪些内容,所以功能则是由各个Module来具体提供。

3,View:它等同于MVP模式、MVVM模式中的View。可以通过IRegionManager注入到Region中。

4,Module:可以把一个大项目拆分开来,每一个Module都包含View、数据、模型,主要用于实现复杂业务操作。

5,IModuleManager:主要用于管理模块加载,可以实现动态加载。

6,IEventAggregator:事件处理接口,实现订阅和发布模式,这也是MVVM框架的一般做法。

  Prism是一个比较庞大的组合框架,4.0引入MEF及功能调整以后变得更加强大了,现在可以说是组合框架和应用框架的统一体,而且是微软团队的力量,所以选它是值得保证的。

3,最强大且功能最多的应用框架——Caliburn

下载地址:http://caliburn.codeplex.com/

开发者:Rob Eisenberg

是否支持Silverlight:是

学习资料:我主要参考项目实例、源码、单元测试用例、国外一些博客和社区。

源码截图如下:

2010-10-31 22-50-51

                                             图5

Caliburn中有几个比较重要的概念:

1,在Actions基础上的Commands带有很多功能,其中包括多参数、过滤操作以及异步调用。

2,窗体和控件的生命周期事件也处理得比较好(包括activation、deactivation、shutdown等)。

3,不管是整个框架还是基于这个框架的应用程序的可测性都比较好。

4,提供了很多常用功能,这些在项目当中都比较有用。

5,除了支持MVVM模式之外,还很好的支持MVP模式以及其他的一些变种模式。

6,强大的依赖注入框架以及AOP框架,这里可以灵活选用其中一种。

Caliburn是一个非常强大的MVVM应用框架,对很多功能都提供了灵活且多种实现,不论是项目使用还是研究代码,感觉都受益颇多。

4,轻量级且适用的MVVMLight

下载地址:http://mvvmlight.codeplex.com/

开发者:Laurent Bugnion

是否支持Silverlight:是

学习资料:除了online documentation,我主要参考项目实例、源码、单元测试用例、国外一些博客和社区。另外这三篇文章非常不错,用MVVMLight开发了一个比较完整的Silverlight企业项目

源码截图如下:

2010-10-21 16-22-04

                                             图6

MVVMLight中有几个比较重要的概念:

1,RelayCommand:通过对Command进行封装,使得MVVM模式在WPF和Silverlight上更加容易。你只需要在 ViewModel中定义好各个RelayCommand,然后在View中通过Command来绑定ViewModel中定义好的 RelayCommand,就可以实现像WinForm、ASP.NET事件一样的效果,只不过这里是解除了UI和逻辑的强耦合。

2,Messager:MVVMLight中的Messager作用比较大,前面讲了MVVM模式解除了ViewModel和View的强引用,那么它们如何来进行交互呢?就是靠它来让ViewModel和View来进行通信的。一般我们会定义一个静态AppMessages类来作为通用的一个通信类,原理就是发布订阅模式。

3,EventToCommand:这里就比较类似于附加行为的概念,是在MVVM Light Toolkit V3中开始引入的概念。

4,ICleanup接口:当显示某个View时,需要先调用Cleanup方法清除数据,这也是由于ViewModel和View的耦合隔离产生的一些必要操作。

  MVVMLight是一个非常好用的MVVM框架,提供了VS和Blend的模板及智能感知。它结合MEF使用真的感觉很轻量级且高效,而且提供了WPF和Silverlight的支持,尤其在Silverlight的支持上比较好,所以一般选择轻量级的Silverlight MVVM模式,它比较被看好。

5,功能齐备且易用的MVVM Helpers

下载地址:http://mvvmhelpers.codeplex.com/

开发者:Mark Smith

是否支持Silverlight:否

学习资料:http://www.julmar.com/blog/mark/以及提供的实例、源码及测试用例。

源码截图如下:

2010-10-21 16-25-14

                                             图7

MVVM Helpers又叫JulMar MVVM Helpers + Behaviors,其中有几个比较重要的概念:

1,提供了MVVM模式的基本功能,包括ViewModel、View及Model之间的隔离,另外还提供了一些常用功能。

2,ViewModel的创建使用标签的形式注入,现在也可以引入MEF。

3,IOC/DI的支持,属性都有验证机制,Wait Cursor的支持,当使用完viewmodel之后能及时释放,这样避免内存泄露。

4,提供了常用的Attached Behaviors支持。

5,消息机制的引入,避免强引用产生的耦合。

MVVM Helpers是一个非常适用的MVVM框架,尤其是提供了MVVM常用功能+MEF+Attached Behaviors,所以项目中的问题基本都能解决。

6,功能强大且轻巧的Cinch

下载地址:http://cinch.codeplex.com/

开发者:Sacha Barber

是否支持Silverlight:否

学习资料:http://sachabarber.net/以及提供的实例、源码及测试用例。

  1. A walkthrough of Cinch, and its internals - Part I
  2. A walkthrough of Cinch, and its internals - Part II
  3. How to develop ViewModels using Cinch
  4. How to Unit test ViewModels using Cinch app, including how to test Background work threads which may run within Cinch ViewModels
  5. A Demo app using Cinch

源码截图如下:

2010-10-21 16-34-31

                                             图8

如果仔细研究其代码,你会发现它和上面讲的MVVM Helpers有很多相似的代码,估计是相互参考了一番:-D。Cinch 中有几个比较重要的概念:

1,这个框架在没有MEF出现之前就已经实现了ViewModel和View之间的强引用隔离,它既没有一般IOC的配置,也没有IView来做中转,并且窗体和控件的生命周期事件也处理得比较好,所以在这方面来说是非常不错的。

2,提供了常用的Attached Behaviors支持,另外也提供了一些MVVM常用功能。

3,DI/IOC使用Unity实现,多线程的实现,避免系统出现不可预料的错误。

4,当使用完viewmodel之后能及时释放,这样避免内存泄露,验证机制的加入,常用导航实现。

5,消息机制的引入,避免强引用产生的耦合。

Cinch 是一个非常强大的框架,尤其是它比较着眼整个应用程序的搭建,所以也比较受到青睐。

7,功能简单且易扩展的MVVMFoundation

下载地址:http://mvvmfoundation.codeplex.com/

开发者:Josh Smith

是否支持Silverlight:否

学习资料:http://joshsmithonwpf.wordpress.com/以及提供的实例、源码及测试用例。

源码截图如下:

2010-10-21 16-22-57 

                                             图9

MVVMFoundation中有几个比较重要的概念:

1,Messenger:这里主要用在各种不同的ViewModel之间通信(比如相互关联的ViewModel、主从ViewModel等),当然也可以扩展成ViewModel与View之间进行通信。

2,ObservableObject:这里相当于ViewModelBase的概念,每一个ViewModel继承自该类,调用完成之后立即释放,防止内存泄露。

3,PropertyObserver:主要是对INotifyPropertyChanged.PropertyChanged进行封装,这样封装可以精简代码,同时可以防止不当操作引起的内存泄露。

4,RelayCommand接口:封装command的声明,包括execution执行逻辑,可选的can-execute逻辑等。外部只需要实例化并Binding就可以简单使用。

MVVMFoundation是一个非常简单的MVVM框架,如果你觉得研究源码比较困难,就可以先从这个框架入手,代码简单而且精炼。

8,附加:支持插件式的依赖注入MEF

下载地址:http://mef.codeplex.com/

源码截图如下:

2010-10-21 16-20-52 

                                             图10

9,如何充分利用这些框架

  上面我们已经介绍了七大MVVM框架+支持插件式的依赖注入MEF,其实另外这三个框架WPF Application Framework (WAF)CalciumCoreMVVM(Basic MVVM framework)也很值得研究,由于时间和精力的关系,针对这三个框架圣殿骑士也没有进行仔细的剖析,只是粗略看了一下基本功能。

其他一些框架诸如OnyxnRouteNito MVVMOceanGoodLight就没有太多关注,有兴趣的朋友也可以选择其中某一个或多个框架作为研究和学习,不过还是推荐研究主流的框架,毕竟这些主流框架的开发团队和成熟度都比较强。

上面看到了这么多MVVM框架,那么我们应该怎么去学习和使用呢?简单来说可以归纳为以下几句话:

1,根据具体的项目选择适合的框架,团队和项目有大有小,所以得根据这些来选择具体的框架,其实终归来说,几个框架功能都比较类似。

2,框架不用研究太多,只要适用于项目就行,尤其是熟练使用并根据其提供的TDD测试代码追溯其原理。

3,框架不是万能的,对于某些应用和功能可以对框架扩展,这也开源最大的好处之一。

针对框架的研究,自己也总结了几点:

1,首先看框架的相关介绍,了解相关的背景、功能、架构图以及其他一些相关信息——认识了解。

2,根据介绍查看并调试框架所提供的实例——熟悉功能。

3,自己写一些相关的项目,主要是熟悉该框架,如果说要急于做项目,后面就可以把框架引入到项目当中——具体使用。

4,根据该框架提供的详细单元测试研究其源码,这也是我最喜欢研究这些开源框架的原因——原理剖析。

5,通过上面的步骤认真分析其原理及细节——准备重现。

6,自己也根据之前的思路重复开发这个框架,最好能用TDD——框架复原捷径。

  上面我们谈了一些开源框架相关知识,下一步我们得自己开发一个MVVM框架,一方面是对知识的总结,另一方面也是对知识的再提炼,同时也能使自己的认识提升到另一个高度,紧接下文。

  由于之前自己做了一套框架,但是还没有趋于完善,所以暂时不准备共享出来(主要是WPF和Silverlight版本更替比较频繁且没有加入模板及智能感知)。对于开发一套MVVM框架,具体需要做一下几个操作:

1,要能解决Model、View和ViewModel之间的强关联,这也是核心功能,尤其是View和ViewModel,不管是使用IOC Container还是MEF都行。

2,命令和附加事件的处理,对Command进行封装,满足多参数、方法过滤、泛型、异步回调等,对附加事件进行封装,使它像使用命令一样简便。

3,由于View和ViewModel是弱关联或者是无关联,如何让它们进行通信?这就需要加入Messenger机制。

4,前面引入了消息机制(一般是静态化处理),如何来管理、实例化、清除消息呢?这里得建立一套消息机制。

5,由于前面采用了弱关联或者无关联,并且引入了消息机制,所以需要对操作有日志记录,否则出了问题无法快速定位及追查原因。

6,如何统一管理ObservableObject、PropertyObserver等这些对象呢?所以得自己建立一套变更体系。

7,对常用功能及操作进行封装,提供一些常用类库以及UI Helper等。

  总之,开发MVVM框架不能求全,只要适合项目就行,也不要想一次就能完善整个框架,在使用时不断根据需求扩展才是明智之举。另外附加三点开发MVVM框架心得:

1,充分借鉴其他开源框架,研究各个框架的不足和优势,然后把思想贯穿于自己的框架中,当然有些常用功能代码也可以直接借鉴过来。

2,对于这样一个比较庞大的框架,使用TDD+反复重构无疑会提高开发效率,同时也能提高框架的可维护性和稳定性,这也是团队使用的一个前提。

3,框架会使用一些常用设计模式,有了它们可以让框架更具有扩展性,同时也减少了开发成本、增加了可维护性。比如观察者模式(消息的发布、订阅、触发)、中介者模式(直接通信的类转化为中间类来处理,隔离耦合)、外观模式(提供一个简单的接口出来,在内部进行大量的封装,这样就可以起到易用且功能强大的目的)、装饰模式(原来已经稳定的功能模块,如果在不改变原有结构的基础上进行扩展,无疑这是一个很好的实践)等。

-—————————————————————————————

以下是各种开源控件,就不介绍了。

Fluent Ribbon Control Suite

Microsoft Ribbon for WPF

AvalonDock

PropertyEditor for WPF

WPF Property Grid

PropertyInspectorView

WPF Dynamic Data Display

Swordfist WPF Charts

WPF Toolkit

AvalonEdit

wpf学习资源站点:http://www.cnblogs.com/KnightsWarrior/archive/2010/11/01/1866641.html

只知道表名XXX查该表属于哪个schema、以及该表有哪些列等信息

SELECT * from information_schema.columns WHERE table_name = ‘xxx’;

只知道列名XXX查哪个schema有该列、以及有列名为XXX的表有哪些等

SELECT * from information_schema.columns WHERE column_name = ‘XXX’;

参考链接:http://blog.163.com/ranma2151920@126/blog/static/9502770620130951445162/

因为前一段时间看到 NetAnalyzer 在Windows10系统下UI表现惨不忍睹,所以利用一段时间为了学习一下WPF相关的内容,于是停停写写,用了WPF相关的技术,两个星期做了一个Markdown编辑器,并且集成了:编辑时与网页同步,博客发布,PDF导出等功能。也主要是不忿某款外国软件收费,故有此作。

代码下载地址

https://github.com/Twzy/MarkWord

展示与说明

代码同步编辑

img

博客发布

img

代码说明

博客发布

MarkWord支持博客园和CSDN博客发布,并且可以进行图片同步(无论是本地图片还是网上的图片,都可以同步到博客服务器)。 该功能使用了MetaWeblog技术。使用方法如下,其中图片上传为newMediaObject 接口

复制代码

1 ///


2 /// 文档上传,包括新增与更新 3 ///

4 public static string UploadBlogs(string apiUrl, string BlogId, string userId, string password, string
5 BlogsModel, string postId, string title, string Markdown, bool publish) 6 {
7
8 int procIndex = 1;
9
10 SendMsg(5, procIndex, “准备数据中……”);
11 //转换为html
12 string Blogs = string.Format(“\r\n{0}”,
13 CommonMark.CommonMarkConverter.Convert(Markdown));
14 metaTools.Url = apiUrl; 15
16
17 Post blogsPost = new Post(); 18
19 //分类
20 List<string> tmpCategories = new List<string>();
21 tmpCategories.Add(“”);//添加空分类,是因为部分博客(如csdn)字段这部分为必填字段不添加会产生异常
22 blogsPost.categories = tmpCategories.ToArray(); 23
24 //添加时间
25 blogsPost.dateCreated = DateTime.Now.ToLocalTime(); 26
27 //添加标题
28 blogsPost.title = title; 29
30
31 //指定文章编号
32 blogsPost.postid = postId; 33
34 //内容
35 blogsPost.description = BlogsModel.Contains(“{0}”) ?//必须使用{0}占位符
36 string.Format(BlogsModel, Blogs) : //根据模板生成数据 主要是为了制定Markdown模板
37 BlogsModel + Blogs; //通过前缀方式添加
38
39 //开始查找图片并更新到服务器
40 HtmlDocument htmlDoc = new HtmlDocument(); 41 WebClient webClient = new WebClient(); 42 htmlDoc.LoadHtml(blogsPost.description);
43 var ImgList = htmlDoc.DocumentNode.Descendants(“img”);
44
45 int procCount = 3 + ImgList.Count(); 46
47 SendMsg(procCount, procIndex++, string.Format(“数据分析完成,总共需要上传{0}张图片”, ImgList.Count()));
48 int imgErr = 0;//图片上传错误数量
49 foreach (var i in ImgList) 50 {
51 SendMsg(procCount, procIndex++, “正在上传图片数据……”);
52 //获取图片文件字符串
53 string ImgUrl = i.GetAttributeValue(“src”, “”);
54 if (string.IsNullOrEmpty(ImgUrl))
55 {
56 imgErr++;
57 continue;
58 }
59 try
60 {
61 var imgeData = webClient.DownloadData(ImgUrl);//下载文件
62
63 FileData fd = default(FileData);
64 fd.bits = imgeData;//图片数据
65 fd.name = Path.GetExtension(ImgUrl);//文件名
66 fd.type = string.Format(“image/{0}”, fd.name.Substring(1));
67
68 UrlData obj = metaTools.newMediaObject(BlogId, userId, password, fd); 69 blogsPost.description = blogsPost.description.Replace(ImgUrl, obj.url); 70 }
71 catch
72 {
73 imgErr++;
74 continue;
75 }
76 }
77 try
78 {
79 if (string.IsNullOrWhiteSpace(postId))
80 {
81 SendMsg(procCount, procIndex++, “开始发布文章……”);
82 postId = metaTools.newPost(BlogId, userId, password, blogsPost, publish); 83 }
84 else
85 {
86 SendMsg(procCount, procIndex++, “正在更新文章……”);
87 metaTools.editPost(postId, userId, password, blogsPost, publish);
88 }
89 }
90 catch (Exception ex) 91 {
92 Common.ShowMessage(“博客发送失败”);
93 return postId; 94 }
95
96 if (imgErr == 0)
97 {
98 Common.ShowMessage(“博客发送成功”);
99 } 100 else
101 { 102 Common.ShowMessage(string.Format(“博客发送成功了,但是有{0}张图片发送失败”, imgErr)); 103 } 104 SendMsg(procCount, procCount, “完成”); 105 return postId; 106
107 }

复制代码

具体API实现方法见代码中的BlogsAPI项目

PDF导出

PDF导出功能,使用了HTML转PDF方法 相关DLL已经包含在项目当中了

复制代码

1 //html to Pdf
2 public static void HtmlToPdf(string filePath, string html, bool isOrientation = false)
3 {
4 if (string.IsNullOrEmpty(html))
5 html = “Null”;
6 // 创建全局信息
7 GlobalConfig gc = new GlobalConfig(); 8 gc.SetMargins(new Margins(50, 50, 60, 60))
9 .SetDocumentTitle(“MarkWord”) 10 .SetPaperSize(PaperKind.A4) 11 .SetPaperOrientation(isOrientation) 12 .SetOutlineGeneration(true); 13
14
15 //页面信息
16 ObjectConfig oc = new ObjectConfig(); 17 oc.SetCreateExternalLinks(false) 18 .SetFallbackEncoding(Encoding.UTF8) 19 .SetLoadImages(true) 20 .SetScreenMediaType(true) 21 .SetPrintBackground(true); 22 //.SetZoomFactor(1.5);
23
24 var pechkin = new SimplePechkin(gc); 25 pechkin.Finished += Pechkin_Finished; 26 pechkin.Error += Pechkin_Error; 27 pechkin.ProgressChanged += Pechkin_ProgressChanged; 28 var buf = pechkin.Convert(oc, html); 29
30 if (buf == null) 31 { 32 Common.ShowMessage(“导出异常”); 33 return; 34 } 35
36 try
37 { 38 string fn = filePath; //Path.GetTempFileName() + “.pdf”;
39 FileStream fs = new FileStream(fn, FileMode.Create); 40 fs.Write(buf, 0, buf.Length); 41 fs.Close(); 42
43 //Process myProcess = new Process(); 44 //myProcess.StartInfo.FileName = fn; 45 //myProcess.Start();
46 } 47 catch { } 48 }

复制代码

CommonMark使用

最后就Markdown的转换,在这里我使用了CommonMark,使用方法比较简单

CommonMark.CommonMarkConverter.Convert(“### test”)

编辑与html页面同步原理

在改工具中比较有意思的就是编辑器与Webbrower的页面同步功能,包括页面”无刷新”同步呈现,已经页面同步滚动,在这里使用的是编辑器触发 textEditor_TextChanged 事件和 ScrollViewer 触发的scrViewer_ScrollChanged 分别通过webbrowser 的 InvokeScript 动态调用Js实现的,我们先来看看两个js的内容

同步呈现

function updatePageContent(msg){
document.body.innerHTML= msg;
}

非常简单,只是将转换出来的html直接通过document.body.innerHTML 赋给当前页面既可以了。

同步滚动

function scrollToPageContent(value){
window.scrollTo(0, value * (document.body.scrollHeight - document.body.clientHeight));
}

这部分,是需要通过WPF页面转过来一个对应的页面位移高度与窗口显示高度的一个页面比例,然后在webbrowser中根据该比例计算页面需要偏移量来实现同步移动

而对应的WPF端的代码为

复制代码

1 ///


2 /// 同步呈现 3 ///

4 ///
5 public void LoadBody(string MarkValue) 6 {
7
8 if (winWebDoc.Document == null)
9 return; 10 winWebDoc.Document.InvokeScript(“updatePageContent”, new object[] { CommonMark.CommonMarkConverter.Convert(MarkValue) }); 11 } 12
13 ///
14 /// 文本更变 15 ///

16 ///
17 ///
18 private void textEditor_TextChanged(object sender, EventArgs e) 19 { 20 if (!isLoadFlag) 21 { 22 if (this.textEditor.Text != “” && scrViewer != null) 23 if (scrViewer.ScrollableHeight == scrViewer.VerticalOffset) 24 scrViewer.ScrollToBottom(); 25
26 BLL.FileManager.isChangeFlag = true; 27 } 28 //加载文档
29 if (MarkDoc == null) 30 return; 31 if (Config.Common.WorkType == WorkType.Both) 32 { 33 MarkDoc.LoadBody(this.textEditor.Text); 34 } 35 } 36 ////////////////////////////////////////////////////////////////////////////////// 37 ///
38 /// 同步滚动 39 ///

40 ///
41 public void ScrollAuto(double value) 42 { 43 if (winWebDoc.Document == null) 44 return; 45 winWebDoc.Document.InvokeScript(“scrollToPageContent”, new object[] { value.ToString(System.Globalization.CultureInfo.InvariantCulture) }); 46
47 } 48 //计算比例
49 public double ScrollViewerPositionPercentage 50 { 51 get
52 { 53 double num = this.scrViewer.ExtentHeight - this.scrViewer.ViewportHeight; 54 double result; 55 if (num != 0.0) 56 { 57 result = this.scrViewer.VerticalOffset / num; 58 } 59 else
60 { 61 result = 0.0; 62 } 63 return result; 64 } 65 } 66
67 //触发同步
68 private void scrViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) 69 { 70 if (MarkDoc == null) return; 71 if (Config.Common.WorkType == WorkType.Both) 72 { 73 MarkDoc.ScrollAuto(this.ScrollViewerPositionPercentage); 74 } 75 }

复制代码

至此,Markword 中设计到的内容点已经基本覆盖到了,如有疑问欢迎交流!!!


最后来一发小广告

NetAnalyzer2016网络协议分析软件源码开放购买,可以分析80多种协议,支持http数据还原(包含chunked和gzip数据) ,欢迎大家可以支持一下!!

墨云NetAnalyzer官网
代码购买链接
如有疑问欢迎QQ联系:470200051

祝大家周末愉快

MediatR是一款进程内的消息订阅、发布框架,提供了Send方法用于发布到单个处理程序、Publish方法发布到多个处理程序,使用起来非常方便。目前支持 .NET Framework4.5、.NET Stardand1.3、.NET Stardand2.0等版本,可跨平台使用。

要在项目中使用MediatR,首先要添加引用:nuget install MediatR

在使用MediatR的时候,需要设置一个容器来实例化所有的Handler,因此我们需要与依赖注入框架结合使用,MediatR支持目前主流的依赖注入框架,例如Autofac等,也可以直接使用 .NET Core 的依赖注入框架。

如果使用 .net core的依赖注入,将MediatR添加到容器将会很方便:

services.AddMediatR(typeof(Program).Assembly);

如果是多个程序集,如果是多个程序集:

services.AddMediatR(typeof(Program).Assembly, typeof(HelloWorld).Assembly);

MediatR有两种消息处理模式:

  • Request/Response模式:Message将被单个Handler处理,可以有返回值
  • Notifictaion模式:Message可以被多个Handler处理,无返回值

Request/Response模式

使用起来很简单,首先定义Request消息,方法如下:

public class Ping : IRequest<string> { }

然后,定义它的处理程序:

public class PingHandler : IRequestHandler<Ping, string> {
public Task<string> Handle(Ping request, CancellationToken cancellationToken) {
return Task.FromResult(“Pong”);
}
}

这样就可以了,我们在控制台发送Ping消息:

var response = await mediator.Send(new Ping());
Console.WriteLine(response);

无返回值的消息

当处理消息不需要返回值时,我们应该使用如下方式定义消息:

public class Ping : IRequest { }

对应的消息处理程序如下:

public class PingHandler: AsyncRequestHandler<Ping> {
protected override Task Handle(Ping request, CancellationToken cancellationToken) {

}

}

同步的消息处理

默认情况下消息的处理都是异步的(返回值为Task对象),如果你想要同步执行消息,需要按下面的方式定义消息处理程序:

public class PingHandler : RequestHandler<Ping, string> {
protected override string Handle(Ping request) {
return “Pong”;
}
}

这种模式符合CQRS中Command的处理方式,一个Command只能有一个Handler,因此,在使用CQRS时可以参考。

Notification 模式

Notification模式将消息发布给多个处理程序,消息的处理没有返回值。

消息的定义:

public class HelloWorld : INotification
{
}

多个处理程序:

public class CNReply : INotificationHandler<HelloWorld>
{
public Task Handle(HelloWorld notification, CancellationToken cancellationToken)
{
Console.WriteLine($”CN Reply: Hello from CN”);
return Task.CompletedTask;
}
}

public class USReply : INotificationHandler<HelloWorld>
{
public Task Handle(HelloWorld notification, CancellationToken cancellationToken)
{
Console.WriteLine($”US Reply: Hello from US”);
return Task.CompletedTask;
}
}

然后通过Publish方法发布消息:

await mediator.Publish(helloworld);

发布策略

默认情况下,MediatR的消息发布是一个一个执行的,即便是返回Task的情况,也是使用await等待上一个执行完成后才进行下一个的调用。如果需要使用并行的方法进行调用,可以进行定制,具体可参考官方示例:MediatR.Examples.PublishStrategies

多态支持

MediatR消息处理程序是支持逆变的,例如我们可以定义一个消息监听程序,监听所有发布的Notification:

public class MessageListener : INotificationHandler<INotification>
{
public Task Handle(INotification notification, CancellationToken cancellationToken)
{
Console.WriteLine($”接收到新的消息:{notification.GetType()}”);

    return Task.CompletedTask;
}

}

对于IRequest类型的消息,此种方式未验证成功。如果可以的话,倒是可以做一个无处理程序的消息的监听,也是挺好玩的。

异步

对于MediatR来说,无论是发送IRequest类型消息,还是发布INotification类型消息,都是异步的。这里需要特别留意,即使你使用的是同步的消息处理程序,对于消息发布来说,都是异步的,与你的处理程序是同步或异步无关。

参考文档

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

Microsoft .Net Remoting系列教程之一:.Net Remoting基础篇_自学过程_脚本之家

Excerpt

本文主要讲解.Net Remoting的基础,需要的朋友可以参考下。


一、Remoting基础

  什么是Remoting,简而言之,我们可以将其看作是一种分布式处理方式。从微软的产品角度来看,可以说Remoting就是DCOM的一种升级,它改善了很多功能,并极好的融合到.Net平台下。Microsoft .NET Remoting 提供了一种允许对象通过应用程序域与另一对象进行交互的框架。这也正是我们使用Remoting的原因。为什么呢?在Windows操作系统中,是将应用程序分离为单独的进程。这个进程形成了应用程序代码和数据周围的一道边界。如果不采用进程间通信(RPC)机制,则在一个进程中执行的代码就不能访问另一进程。这是一种操作系统对应用程序的保护机制。然而在某些情况下,我们需要跨过应用程序域,与另外的应用程序域进行通信,即穿越边界。

  在Remoting中是通过通道(channel)来实现两个应用程序域之间对象的通信的。如图所示:

//img.jbzj.com/file_images/article/201605/201653092451348.jpg

  首先,客户端通过Remoting,访问通道以获得服务端对象,再通过代理解析为客户端对象。这就提供一种可能性,即以服务的方式来发布服务器对象。远程对象代码可以运行在服务器上(如服务器激活的对象和客户端激活的对象),然后客户端再通过Remoting连接服务器,获得该服务对象并通过序列化在客户端运行。

  在Remoting中,对于要传递的对象,设计者除了需要了解通道的类型和端口号之外,无需再了解数据包的格式。但必须注意的是,客户端在获取服务器端对象时,并不是获得实际的服务端对象,而是获得它的引用。这既保证了客户端和服务器端有关对象的松散耦合,同时也优化了通信的性能。

1、Remoting的两种通道

  Remoting的通道主要有两种:Tcp和Http。在.Net中,System.Runtime.Remoting.Channel中定义了IChannel接口。IChannel接口包括了TcpChannel通道类型和Http通道类型。它们分别对应Remoting通道的这两种类型。

  TcpChannel类型放在名字空间System.Runtime.Remoting.Channel.Tcp中。Tcp通道提供了基于Socket的传输工具,使用Tcp协议来跨越Remoting边界传输序列化的消息流。TcpChannel类型默认使用二进制格式序列化消息对象,因此它具有更高的传输性能。HttpChannel类型放在名字空间System.Runtime.Remoting.Channel.Http中。它提供了一种使用Http协议,使其能在Internet上穿越防火墙传输序列化消息流。默认情况下,HttpChannel类型使用Soap格式序列化消息对象,因此它具有更好的互操作性。通常在局域网内,我们更多地使用TcpChannel;如果要穿越防火墙,则使用HttpChannel。

2、远程对象的激活方式

  在访问远程类型的一个对象实例之前,必须通过一个名为Activation的进程创建它并进行初始化。这种客户端通过通道来创建远程对象,称为对象的激活。在Remoting中,远程对象的激活分为两大类:服务器端激活和客户端激活。

  (1) 服务器端激活,又叫做WellKnow方式,很多又翻译为知名对象。为什么称为知名对象激活模式呢?是因为服务器应用程序在激活对象实例之前会在一个众所周知的统一资源标识符(URI)上来发布这个类型。然后该服务器进程会为此类型配置一个WellKnown对象,并根据指定的端口或地址来发布对象。.Net Remoting把服务器端激活又分为SingleTon模式和SingleCall模式两种。

  SingleTon模式:此为有状态模式。如果设置为SingleTon激活方式,则Remoting将为所有客户端建立同一个对象实例。当对象处于活动状态时,SingleTon实例会处理所有后来的客户端访问请求,而不管它们是同一个客户端,还是其他客户端。SingleTon实例将在方法调用中一直维持其状态。举例来说,如果一个远程对象有一个累加方法(i=0;++i),被多个客户端(例如两个)调用。如果设置为SingleTon方式,则第一个客户获得值为1,第二个客户获得值为2,因为他们获得的对象实例是相同的。如果熟悉Asp.Net的状态管理,我们可以认为它是一种Application状态。

  SingleCall模式:SingleCall是一种无状态模式。一旦设置为SingleCall模式,则当客户端调用远程对象的方法时,Remoting会为每一个客户端建立一个远程对象实例,至于对象实例的销毁则是由GC自动管理的。同上一个例子而言,则访问远程对象的两个客户获得的都是1。我们仍然可以借鉴Asp.Net的状态管理,认为它是一种Session状态。

  (2) 客户端激活。与WellKnown模式不同,Remoting在激活每个对象实例的时候,会给每个客户端激活的类型指派一个URI。客户端激活模式一旦获得客户端的请求,将为每一个客户端都建立一个实例引用。SingleCall模式和客户端激活模式是有区别的:首先,对象实例创建的时间不一样。客户端激活方式是客户一旦发出调用的请求,就实例化;而SingleCall则是要等到调用对象方法时再创建。其次,SingleCall模式激活的对象是无状态的,对象生命期的管理是由GC管理的,而客户端激活的对象则有状态,其生命周期可自定义。其三,两种激活模式在服务器端和客户端实现的方法不一样。尤其是在客户端,SingleCall模式是由GetObject()来激活,它调用对象默认的构造函数。而客户端激活模式,则通过CreateInstance()来激活,它可以传递参数,所以可以调用自定义的构造函数来创建实例。

二、远程对象的定义

  前面讲到,客户端在获取服务器端对象时,并不是获得实际的服务端对象,而是获得它的引用。因此在Remoting中,对于远程对象有一些必须的定义规范要遵循。

  由于Remoting传递的对象是以引用的方式,因此所传递的远程对象类必须继承MarshalByRefObject。MSDN对MarshalByRefObject的说明是:MarshalByRefObject 是那些通过使用代理交换消息来跨越应用程序域边界进行通信的对象的基类。不是从 MarshalByRefObject 继承的对象会以隐式方式按值封送。当远程应用程序引用一个按值封送的对象时,将跨越远程处理边界传递该对象的副本。因为您希望使用代理方法而不是副本方法进行通信,因此需要继承MarshallByRefObject。

以下是一个远程对象类的定义:

1

2

3

4

5

6

7

8

9

10

11

public class ServerObject:MarshalByRefObject

{

 public Person GetPersonInfo(string name,string sex,int age)

 {

 Person person = new Person();

 person.Name = name;

 person.Sex = sex;

 person.Age = age;

 return person;

 }

}

  这个类只实现了最简单的方法,就是设置一个人的基本信息,并返回一个Person类对象。注意这里返回的Person类。由于这里所传递的Person则是以传值的方式来完成的,而Remoting要求必须是引用的对象,所以必须将Person类序列化。

  因此,在Remoting中的远程对象中,如果还要调用或传递某个对象,例如类,或者结构,则该类或结构则必须实现串行化

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

Attribute[SerializableAttribute]:

[Serializable]

public class Person

{

 public Person()

 {

 }

 private string name;

 private string sex;

 private int age;

 public string Name

 {

 get {return name;}

 set {name = value;}

 }

 public string Sex

 {

 get {return sex;}

 set {sex = value;}

 }

 public int Age

 {

 get {return age;}

 set {age = value;}

 }

}

  将该远程对象以类库的方式编译成Dll。这个Dll将分别放在服务器端和客户端,以添加引用。

  在Remoting中能够传递的远程对象可以是各种类型,包括复杂的DataSet对象,只要它能够被序列化。远程对象也可以包含事件,但服务器端对于事件的处理比较特殊,我将在本系列之三中介绍。

三、服务器端

根据第一部分所述,根据激活模式的不同,通道类型的不同服务器端的实现方式也有所不同。大体上说,服务器端应分为三步:

1、注册通道

  要跨越应用程序域进行通信,必须实现通道。如前所述,Remoting提供了IChannel接口,分别包含TcpChannel和HttpChannel两种类型的通道。这两种类型除了性能和序列化数据的格式不同外,实现的方式完全一致,因此下面我们就以TcpChannel为例。

  注册TcpChannel,首先要在项目中添加引用“System.Runtime.Remoting”,然后using名字空间:System.Runtime.Remoting.Channel.Tcp。代码如下:

1

2

TcpChannel channel = new TcpChannel(8080);

ChannelServices.RegisterChannel(channel);

在实例化通道对象时,将端口号作为参数传递。然后再调用静态方法RegisterChannel()来注册该通道对象即可。

2、注册远程对象

注册了通道后,要能激活远程对象,必须在通道中注册该对象。根据激活模式的不同,注册对象的方法也不同。

(1) SingleTon模式

对于WellKnown对象,可以通过静态方法RemotingConfiguration.RegisterWellKnownServiceType()来实现:

1

2

3

RemotingConfiguration.RegisterWellKnownServiceType(

 typeof(ServerRemoteObject.ServerObject),

 "ServiceMessage",WellKnownObjectMode.SingleTon);

(2)SingleCall模式

注册对象的方法基本上和SingleTon模式相同,只需要将枚举参数WellKnownObjectMode改为SingleCall就可以了。

1

2

3

RemotingConfiguration.RegisterWellKnownServiceType(

 typeof(ServerRemoteObject.ServerObject),

 "ServiceMessage",WellKnownObjectMode.SingleCall);

(3)客户端激活模式

对于客户端激活模式,使用的方法又有不同,但区别不大,看了代码就一目了然。

1

2

3

RemotingConfiguration.ApplicationName = "ServiceMessage";

RemotingConfiguration.RegisterActivatedServiceType(

 typeof(ServerRemoteObject.ServerObject));

  为什么要在注册对象方法前设置ApplicationName属性呢?其实这个属性就是该对象的URI。对于WellKnown模式,URI是放在RegisterWellKnownServiceType()方法的参数中,当然也可以拿出来专门对ApplicationName属性赋值。而RegisterActivatedServiceType()方法的重载中,没有ApplicationName的参数,所以必须分开。

3、注销通道

  如果要关闭Remoting的服务,则需要注销通道,也可以关闭对通道的监听。在Remoting中当我们注册通道的时候,就自动开启了通道的监听。而如果关闭了对通道的监听,则该通道就无法接受客户端的请求,但通道仍然存在,如果你想再一次注册该通道,会抛出异常。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

IChannel[] channels = ChannelServices.RegisteredChannels;

foreach (IChannel eachChannel in channels)

{

 if (eachChannel.ChannelName == "MyTcp")

 {

 TcpChannel tcpChannel = (TcpChannel)eachChannel;

 tcpChannel.StopListening(null);

 ChannelServices.UnregisterChannel(tcpChannel);

 }

}

  代码中,RegisterdChannel属性获得的是当前已注册的通道。在Remoting中,是允许同时注册多个通道的,这一点会在后面说明。

四、客户端

  客户端主要做两件事,一是注册通道。这一点从图一就可以看出,Remoting中服务器端和客户端都必须通过通道来传递消息,以获得远程对象。第二步则是获得该远程对象。

1、注册通道:

1

2

TcpChannel channel = new TcpChannel();

ChannelServices.RegisterChannel(channel);

  注意在客户端实例化通道时,是调用的默认构造函数,即没有传递端口号。事实上,这个端口号是缺一不可的,只不过它的指定被放在后面作为了Uri的一部分。

2、获得远程对象。

  与服务器端相同,不同的激活模式决定了客户端的实现方式也将不同。不过这个区别仅仅是WellKnown激活模式和客户端激活模式之间的区别,而对于SingleTon和SingleCall模式,客户端的实现完全相同。

(1) WellKnown激活模式

  要获得服务器端的知名远程对象,可通过Activator进程的GetObject()方法来获得:

1

2

ServerRemoteObject.ServerObject serverObj = (ServerRemoteObject.ServerObject)Activator.GetObject(

 typeof(ServerRemoteObject.ServerObject), "tcp://localhost:8080/ServiceMessage");

  首先以WellKnown模式激活,客户端获得对象的方法是使用GetObject()。其中参数第一个是远程对象的类型。第二个参数就是服务器端的uri。如果是http通道,自然是用http://localhost:8080/ServiceMessage了。因为我是用本地机,所以这里是localhost,你可以用具体的服务器IP地址来代替它。端口必须和服务器端的端口一致。后面则是服务器定义的远程对象服务名,即ApplicationName属性的内容。

(2) 客户端激活模式

  如前所述,WellKnown模式在客户端创建对象时,只能调用默认的构造函数,上面的代码就说明了这一点,因为GetObject()方法不能传递构造函数的参数。而客户端激活模式则可以通过自定义的构造函数来创建远程对象。

客户端激活模式有两种方法:
1) 调用RemotingConfiguration的静态方法RegisterActivatedClientType()。这个方法返回值为Void,它只是将远程对象注册在客户端而已。具体的实例化还需要调用对象类的构造函数。

1

2

3

4

RemotingConfiguration.RegisterActivatedClientType(   

 typeof(ServerRemoteObject.ServerObject),

 "tcp://localhost:8080/ServiceMessage");

ServerRemoteObject.ServerObject serverObj = new ServerRemoteObject.ServerObject();

2) 调用进程Activator的CreateInstance()方法。这个方法将创建方法参数指定类型的类对象。它与前面的GetObject()不同的是,它要在客户端调用构造函数,而GetObject()只是获得对象,而创建实例是在服务器端完成的。CreateInstance()方法有很多个重载,我着重说一下其中常用的两个。

a、 public static object CreateInstance(Type type, object[] args, object[] activationAttributes);

参数说明:
type:要创建的对象的类型。
args :与要调用构造函数的参数数量、顺序和类型匹配的参数数组。如果 args 为空数组或空引用(Visual Basic 中为 Nothing),则调用不带任何参数的构造函数(默认构造函数)。
activationAttributes :包含一个或多个可以参与激活的属性的数组。

  这里的参数args是一个object[]数组类型。它可以传递要创建对象的构造函数中的参数。从这里其实可以得到一个结论:WellKnown激活模式所传递的远程对象类,只能使用默认的构造函数;而Activated模式则可以用户自定义构造函数。activationAttributes参数在这个方法中通常用来传递服务器的url。
假设我们的远程对象类ServerObject有个构造函数:

1

2

3

4

5

6

ServerObject(string pName,string pSex,int pAge)

{

 name = pName;

 sex = pSex;

 age = pAge;

}

那么实现的代码是:

1

2

3

4

5

6

7

object[] attrs = {new UrlAttribute("tcp://localhost:8080/ServiceMessage")};

object[] objs = new object[3];

objs[0] = "wayfarer";

objs[1] = "male";

objs[2] = 28;

ServerRemoteObject.ServerObject = Activator.CreateInstance(

 typeof(ServerRemoteObject.ServerObject),objs,attrs);

可以看到,objs[]数组传递的就是构造函数的参数。

b、public static ObjectHandle CreateInstance(string assemblyName, string typeName, object[] activationAttribute);

参数说明:
assemblyName :将在其中查找名为 typeName 的类型的程序集的名称。如果 assemblyName 为空引用(Visual Basic 中为 Nothing),则搜索正在执行的程序集。
typeName:首选类型的名称。
activationAttributes :包含一个或多个可以参与激活的属性的数组。

参数说明一目了然。注意这个方法返回值为ObjectHandle类型,因此代码与前不同:

1

2

3

4

object[] attrs = {new UrlAttribute("tcp://localhost:8080/EchoMessage")};  

ObjectHandle handle = Activator.CreateInstance("ServerRemoteObject",

 "ServerRemoteObject.ServerObject",attrs);

ServerRemoteObject.ServerObject obj = (ServerRemoteObject.ServerObject)handle.Unwrap();

这个方法实际上是调用的默认构造函数。ObjectHandle.Unwrap()方法是返回被包装的对象。

说明:要使用UrlAttribute,还需要在命名空间中添加:using System.Runtime.Remoting.Activation;

五、Remoting基础的补充

  通过上面的描述,基本上已经完成了一个最简单的Remoting程序。这是一个标准的创建Remoting程序的方法,但在实际开发过程中,我们遇到的情况也许千奇百怪,如果只掌握一种所谓的“标准”,就妄想可以“一招鲜、吃遍天”,是不可能的。

1、注册多个通道

  在Remoting中,允许同时创建多个通道,即根据不同的端口创建不同的通道。但是,Remoting要求通道的名字必须不同,因为它要用来作为通道的唯一标识符。虽然IChannel有ChannelName属性,但这个属性是只读的。因此前面所述的创建通道的方法无法实现同时注册多个通道的要求。

这个时候,我们必须用到System.Collection中的IDictionary接口:

注册Tcp通道:

1

2

3

4

5

6

7

IDictionary tcpProp = new Hashtable();

tcpProp["name"] = "tcp9090";

tcpProp["port"] = 9090;

IChannel channel = new TcpChannel(tcpProp,

 new BinaryClientFormatterSinkProvider(),

 new BinaryServerFormatterSinkProvider());

ChannelServices.RegisterChannel(channel);

注册Http通道:

1

2

3

4

5

6

7

IDictionary httpProp = new Hashtable();

httpProp["name"] = "http8080";

httpProp["port"] = 8080;

IChannel channel = new HttpChannel(httpProp,

 new SoapClientFormatterSinkProvider(),

 new SoapServerFormatterSinkProvider());

ChannelServices.RegisterChannel(channel);

在name属性中,定义不同的通道名称就可以了。

2、远程对象元数据相关性

  由于服务器端和客户端都要用到远程对象,通常的方式是生成两份完全相同的对象Dll,分别添加引用。不过为了代码的安全性,且降低客户端对远程对象元数据的相关性,我们有必要对这种方式进行改动。即在服务器端实现远程对象,而在客户端则删除这些实现的元数据。

由于激活模式的不同,在客户端创建对象的方法也不同,所以要分离元数据的相关性,也应分为两种情况。

(1) WellKnown激活模式:

通过接口来实现。在服务器端,提供接口和具体类的实现,而在客户端仅提供接口:

1

2

3

4

5

6

7

public interface IServerObject

{

 Person GetPersonInfo(string name,string sex,int age);

}

public class ServerObject:MarshalByRefObject,IServerObject

{ ......}

注意:两边生成该对象程序集的名字必须相同,严格地说,是命名空间的名字必须相同。

           (2) 客户端激活模式:

  如前所述,对于客户端激活模式,不管是使用静态方法,还是使用CreateInstance()方法,都必须在客户端调用构造函数实例化对象。所以,在客户端我们提供的远程对象,就不能只提供接口,而没有类的实现。实际上,要做到与远程对象元数据的分离,可以由两种方法供选择:

a、利用WellKnown激活模式模拟客户端激活模式:

方法是利用设计模式中的“抽象工厂”,下面的类图表描述了总体解决方案:

//img.jbzj.com/file_images/article/201605/201653092501685.gif

我们在服务器端的远程对象中加上抽象工厂的接口和实现类:

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

public interface IServerObject

{

 Person GetPersonInfo(string name,string sex,int age);

}

public interface IServerObjFactory

{

 IServerObject CreateInstance(); 

}

public class ServerObject:MarshalByRefObject,IServerObject

{

 public Person GetPersonInfo(string name,string sex,int age)

 {

  Person person = new Person();

  person.Name = name;

  person.Sex = sex;

  person.Age = age;

  return person;

 

}

public class ServerObjFactory:MarshalByRefObject,IServerObjFactory

{

 public IServerObject CreateInstance()

 {

  return new ServerObject();

 }

}

然后再客户端的远程对象中只提供工厂接口和原来的对象接口:

1

2

3

4

5

6

7

8

9

public interface IServerObject

{

 Person GetPersonInfo(string name,string sex,int age);

}

public interface IServerObjFactory

{

 IServerObject CreateInstance(); 

}

我们用WellKnown激活模式注册远程对象,在服务器端:

1

2

3

4

RemotingConfiguration.RegisterWellKnownServiceType(

 typeof(ServerRemoteObject.ServerObjFactory),

 "ServiceMessage",WellKnownObjectMode.SingleCall);

注意这里注册的不是ServerObject类对象,而是ServerObjFactory类对象。

客户端:

1

2

3

4

5

6

ServerRemoteObject.IServerObjFactory serverFactory =   

 (ServerRemoteObject.IServerObjFactory) Activator.GetObject(

 typeof(ServerRemoteObject.IServerObjFactory),

 "tcp://localhost:8080/ServiceMessage");

ServerRemoteObject.IServerObject serverObj = serverFactory.CreateInstance();

  为什么说这是一种客户端激活模式的模拟呢?从激活的方法来看,我们是使用了SingleCall模式来激活对象,但此时激活的并非我们要传递的远程对象,而是工厂对象。如果客户端要创建远程对象,还应该通过工厂对象的CreateInstance()方法来获得。而这个方法正是在客户端调用的。因此它的实现方式就等同于客户端激活模式。

b、利用替代类来取代远程对象的元数据

  实际上,我们可以用一个trick,来欺骗Remoting。这里所说的替代类就是这个trick了。既然是提供服务,Remoting传递的远程对象其实现的细节当然是放在服务器端。而要在客户端放对象的副本,不过是因为客户端必须调用构造函数,而采取的无奈之举。既然具体的实现是在服务器端,又为了能在客户端实例化,那么在客户端就实现这些好了。至于实现的细节,就不用管了。

  如果远程对象有方法,服务器端则提供方法实现,而客户端就提供这个方法就OK了,至于里面的实现,你可以是抛出一个异常,或者return 一个null值;如果方法返回void,那么里面可以是空。关键是这个客户端类对象要有这个方法。这个方法的实现,其实和方法的声明差不多,所以我说是一个trick。方法如是,构造函数也如此。

还是用代码来说明这种“阴谋”,更直观:

服务器端:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public class ServerObject:MarshalByRefObject

{

 public ServerObject()

 {

 }

 public Person GetPersonInfo(string name,string sex,int age)

 {

  Person person = new Person();

  person.Name = name;

  person.Sex = sex;

  person.Age = age;

  return person;

 

}

客户端:

1

2

3

4

5

6

7

8

9

10

11

12

public class ServerObject:MarshalByRefObject

{

 public ServerObj()

 {

  throw new System.NotImplementedException();

 }

 public Person GetPersonInfo(string name,string sex,int age)

 {

  throw new System.NotImplementedException();

 

}

  比较客户端和服务器端,客户端的方法GetPersonInfo(),没有具体的实现细节,只是抛出了一个异常。或者直接写上语句return null,照样OK。我们称客户端的这个类为远程对象的替代类。

3、利用配置文件实现

  前面所述的方法,于服务器uri、端口、以及激活模式的设置是用代码来完成的。其实我们也可以用配置文件来设置。这样做有个好处,因为这个配置文件是Xml文档。如果需要改变端口或其他,我们就不需要修改程序,并重新编译,而是只需要改变这个配置文件即可。

(1) 服务器端的配置文件:

1

2

3

4

5

6

7

8

9

10

11

12

<configuration>

 <system.runtime.remoting>

 <application name="ServerRemoting">

  <service>

  <wellknown mode="Singleton" type="ServerRemoteObject.ServerObject" objectUri="ServiceMessage"/>

  </service>

  <channels>

   <channel ref="tcp" port="8080"/>

  </channels>

 </application>

 </system.runtime.remoting>

</configuration>

如果是客户端激活模式,则把wellknown改为activated,同时删除mode属性。

把该配置文件放到服务器程序的应用程序文件夹中,命名为ServerRemoting.config。那么前面的服务器端程序直接用这条语句即可:

1

RemotingConfiguration.Configure("ServerRemoting.config");

(2) 客户端配置文件

如果是客户端激活模式,修改和上面一样。调用也是使用RemotingConfiguration.Configure()方法来调用存储在客户端的配置文件。

配置文件还可以放在machine.config中。如果客户端程序是web应用程序,则可以放在web.config中。

4、启动/关闭指定远程对象

  Remoting中没有提供类似UnregisterWellKnownServiceType()的方法,也即是说,一旦通过注册了远程对象,如果没有关闭通道的话,该对象就一直存在于通道中。只要客户端激活该对象,就会创建对象实例。如果Remoting传送的只有一个远程对象,这不存在问题,关闭通道就可以了。如果传送多个远程对象呢?要关闭指定的远程对象应该怎么做?关闭之后又需要启动又该如何?

  我们注意到在Remoting中提供了Marshal()和Disconnect()方法,答案就在这里。Marshal()方法是将MarshalByRefObject类对象转化为ObjRef类对象,这个对象是存储生成代理以与远程对象通讯所需的所有相关信息。这样就可以将该实例序列化以便在应用程序域之间以及通过网络进行传输,客户端就可以调用了。而Disconnect()方法则将具体的实例对象从通道中断开。

方法如下:
首先注册通道:

1

2

TcpChannel channel = new TcpChannel(8080);

ChannelServices.RegisterChannel(channel);

接着启动服务:
先在服务器端实例化远程对象。

ServerObject obj = new ServerObject();

然后,注册该对象。注意这里不用RemotingConfiguration.RegisterWellKnownServiceType(),而是使用RemotingServices.Marshal():

ObjRef objrefWellKnown = RemotingServices.Marshal(obj, “ServiceMessage”);

如果要注销对象,则:

RemotingServices.Disconnect(obj);

  要注意,这里Disconnect的类对象必须是前面实例化的对象。正因为此,我们可以根据需要创建指定的远程对象,而关闭时,则Disconnect之前实例化的对象。

  至于客户端的调用,和前面WellKnown模式的方法相同,仍然是通过Activator.GetObject()来获得。但从实现代码来看,我们会注意到一个问题,由于服务器端是显式的实例化了远程对象,因此不管客户端有多少,是否相同,它们调用的都是同一个远程对象。因此我们将这个方法称为模拟的SingleTon模式。

客户端激活模式

  我们也可以通过Marshal()和Disconnect()来模拟客户端激活模式。首先我们来回顾“远程对象元数据相关性”一节,在这一节中,我说到采用设计模式的“抽象工厂”来创建对象实例,以此用SingleCall模式来模拟客户端激活模式。在仔细想想前面的模拟的SingleTon模式。是不是答案就将呼之欲出呢?

  在“模拟的SingleTon”模式中,我们是将具体的远程对象实例进行Marshal,以此让客户端获得该对象的引用信息。那么我们换一种思路,当我们用抽象工厂提供接口,工厂类实现创建远程对象的方法。然后我们在服务器端创建工厂类实例。再将这个工厂类实例进行Marshal。而客户端获取对象时,不是获取具体的远程对象,而是获取具体的工厂类对象。然后再调用CreateInstance()方法来创建具体的远程对象实例。此时,对于多个客户端而言,调用的是同一个工厂类对象;然而远程对象是在各个客户端自己创建的,因此对于远程对象而言,则是由客户端激活,创建的是不同对象了。

当我们要启动/关闭指定对象时,只需要用Disconnet()方法来注销工厂类对象就可以了。

六、小结

  Microsoft.Net Remoting真可以说是博大精深。整个Remoting的内容不是我这一篇小文所能尽述的,更不是我这个Remoting的初学者所能掌握的。王国维在《人间词话》一书中写到:古今之成大事业大学问者,必经过三种境界。“昨夜西风凋碧树,独上高楼,望尽天涯路。”此第一境界也。“衣带渐宽终不悔,为伊消得人憔悴。”此第二境界也。“众里寻他千百度,蓦然回首,那人却在灯火阑珊处。”此第三境界也。如以此来形容我对Remoting的学习,还处于“独上高楼,望尽天涯路”的时候,真可以说还未曾登堂入室。

  或许需得“衣带渐宽”,学得Remoting“终不悔”,方才可以“蓦然回首”吧。

  以上就是.Net Remoting基础的全部内容,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:

Microsoft .Net Remoting系列教程之一:.Net Remoting基础篇_自学过程_脚本之家

Excerpt

本文主要讲解.Net Remoting的基础,需要的朋友可以参考下。


一、Remoting基础

  什么是Remoting,简而言之,我们可以将其看作是一种分布式处理方式。从微软的产品角度来看,可以说Remoting就是DCOM的一种升级,它改善了很多功能,并极好的融合到.Net平台下。Microsoft .NET Remoting 提供了一种允许对象通过应用程序域与另一对象进行交互的框架。这也正是我们使用Remoting的原因。为什么呢?在Windows操作系统中,是将应用程序分离为单独的进程。这个进程形成了应用程序代码和数据周围的一道边界。如果不采用进程间通信(RPC)机制,则在一个进程中执行的代码就不能访问另一进程。这是一种操作系统对应用程序的保护机制。然而在某些情况下,我们需要跨过应用程序域,与另外的应用程序域进行通信,即穿越边界。

  在Remoting中是通过通道(channel)来实现两个应用程序域之间对象的通信的。如图所示:

//img.jbzj.com/file_images/article/201605/201653092451348.jpg

  首先,客户端通过Remoting,访问通道以获得服务端对象,再通过代理解析为客户端对象。这就提供一种可能性,即以服务的方式来发布服务器对象。远程对象代码可以运行在服务器上(如服务器激活的对象和客户端激活的对象),然后客户端再通过Remoting连接服务器,获得该服务对象并通过序列化在客户端运行。

  在Remoting中,对于要传递的对象,设计者除了需要了解通道的类型和端口号之外,无需再了解数据包的格式。但必须注意的是,客户端在获取服务器端对象时,并不是获得实际的服务端对象,而是获得它的引用。这既保证了客户端和服务器端有关对象的松散耦合,同时也优化了通信的性能。

1、Remoting的两种通道

  Remoting的通道主要有两种:Tcp和Http。在.Net中,System.Runtime.Remoting.Channel中定义了IChannel接口。IChannel接口包括了TcpChannel通道类型和Http通道类型。它们分别对应Remoting通道的这两种类型。

  TcpChannel类型放在名字空间System.Runtime.Remoting.Channel.Tcp中。Tcp通道提供了基于Socket的传输工具,使用Tcp协议来跨越Remoting边界传输序列化的消息流。TcpChannel类型默认使用二进制格式序列化消息对象,因此它具有更高的传输性能。HttpChannel类型放在名字空间System.Runtime.Remoting.Channel.Http中。它提供了一种使用Http协议,使其能在Internet上穿越防火墙传输序列化消息流。默认情况下,HttpChannel类型使用Soap格式序列化消息对象,因此它具有更好的互操作性。通常在局域网内,我们更多地使用TcpChannel;如果要穿越防火墙,则使用HttpChannel。

2、远程对象的激活方式

  在访问远程类型的一个对象实例之前,必须通过一个名为Activation的进程创建它并进行初始化。这种客户端通过通道来创建远程对象,称为对象的激活。在Remoting中,远程对象的激活分为两大类:服务器端激活和客户端激活。

  (1) 服务器端激活,又叫做WellKnow方式,很多又翻译为知名对象。为什么称为知名对象激活模式呢?是因为服务器应用程序在激活对象实例之前会在一个众所周知的统一资源标识符(URI)上来发布这个类型。然后该服务器进程会为此类型配置一个WellKnown对象,并根据指定的端口或地址来发布对象。.Net Remoting把服务器端激活又分为SingleTon模式和SingleCall模式两种。

  SingleTon模式:此为有状态模式。如果设置为SingleTon激活方式,则Remoting将为所有客户端建立同一个对象实例。当对象处于活动状态时,SingleTon实例会处理所有后来的客户端访问请求,而不管它们是同一个客户端,还是其他客户端。SingleTon实例将在方法调用中一直维持其状态。举例来说,如果一个远程对象有一个累加方法(i=0;++i),被多个客户端(例如两个)调用。如果设置为SingleTon方式,则第一个客户获得值为1,第二个客户获得值为2,因为他们获得的对象实例是相同的。如果熟悉Asp.Net的状态管理,我们可以认为它是一种Application状态。

  SingleCall模式:SingleCall是一种无状态模式。一旦设置为SingleCall模式,则当客户端调用远程对象的方法时,Remoting会为每一个客户端建立一个远程对象实例,至于对象实例的销毁则是由GC自动管理的。同上一个例子而言,则访问远程对象的两个客户获得的都是1。我们仍然可以借鉴Asp.Net的状态管理,认为它是一种Session状态。

  (2) 客户端激活。与WellKnown模式不同,Remoting在激活每个对象实例的时候,会给每个客户端激活的类型指派一个URI。客户端激活模式一旦获得客户端的请求,将为每一个客户端都建立一个实例引用。SingleCall模式和客户端激活模式是有区别的:首先,对象实例创建的时间不一样。客户端激活方式是客户一旦发出调用的请求,就实例化;而SingleCall则是要等到调用对象方法时再创建。其次,SingleCall模式激活的对象是无状态的,对象生命期的管理是由GC管理的,而客户端激活的对象则有状态,其生命周期可自定义。其三,两种激活模式在服务器端和客户端实现的方法不一样。尤其是在客户端,SingleCall模式是由GetObject()来激活,它调用对象默认的构造函数。而客户端激活模式,则通过CreateInstance()来激活,它可以传递参数,所以可以调用自定义的构造函数来创建实例。

二、远程对象的定义

  前面讲到,客户端在获取服务器端对象时,并不是获得实际的服务端对象,而是获得它的引用。因此在Remoting中,对于远程对象有一些必须的定义规范要遵循。

  由于Remoting传递的对象是以引用的方式,因此所传递的远程对象类必须继承MarshalByRefObject。MSDN对MarshalByRefObject的说明是:MarshalByRefObject 是那些通过使用代理交换消息来跨越应用程序域边界进行通信的对象的基类。不是从 MarshalByRefObject 继承的对象会以隐式方式按值封送。当远程应用程序引用一个按值封送的对象时,将跨越远程处理边界传递该对象的副本。因为您希望使用代理方法而不是副本方法进行通信,因此需要继承MarshallByRefObject。

以下是一个远程对象类的定义:

1

2

3

4

5

6

7

8

9

10

11

public class ServerObject:MarshalByRefObject

{

 public Person GetPersonInfo(string name,string sex,int age)

 {

 Person person = new Person();

 person.Name = name;

 person.Sex = sex;

 person.Age = age;

 return person;

 }

}

  这个类只实现了最简单的方法,就是设置一个人的基本信息,并返回一个Person类对象。注意这里返回的Person类。由于这里所传递的Person则是以传值的方式来完成的,而Remoting要求必须是引用的对象,所以必须将Person类序列化。

  因此,在Remoting中的远程对象中,如果还要调用或传递某个对象,例如类,或者结构,则该类或结构则必须实现串行化

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

Attribute[SerializableAttribute]:

[Serializable]

public class Person

{

 public Person()

 {

 }

 private string name;

 private string sex;

 private int age;

 public string Name

 {

 get {return name;}

 set {name = value;}

 }

 public string Sex

 {

 get {return sex;}

 set {sex = value;}

 }

 public int Age

 {

 get {return age;}

 set {age = value;}

 }

}

  将该远程对象以类库的方式编译成Dll。这个Dll将分别放在服务器端和客户端,以添加引用。

  在Remoting中能够传递的远程对象可以是各种类型,包括复杂的DataSet对象,只要它能够被序列化。远程对象也可以包含事件,但服务器端对于事件的处理比较特殊,我将在本系列之三中介绍。

三、服务器端

根据第一部分所述,根据激活模式的不同,通道类型的不同服务器端的实现方式也有所不同。大体上说,服务器端应分为三步:

1、注册通道

  要跨越应用程序域进行通信,必须实现通道。如前所述,Remoting提供了IChannel接口,分别包含TcpChannel和HttpChannel两种类型的通道。这两种类型除了性能和序列化数据的格式不同外,实现的方式完全一致,因此下面我们就以TcpChannel为例。

  注册TcpChannel,首先要在项目中添加引用“System.Runtime.Remoting”,然后using名字空间:System.Runtime.Remoting.Channel.Tcp。代码如下:

1

2

TcpChannel channel = new TcpChannel(8080);

ChannelServices.RegisterChannel(channel);

在实例化通道对象时,将端口号作为参数传递。然后再调用静态方法RegisterChannel()来注册该通道对象即可。

2、注册远程对象

注册了通道后,要能激活远程对象,必须在通道中注册该对象。根据激活模式的不同,注册对象的方法也不同。

(1) SingleTon模式

对于WellKnown对象,可以通过静态方法RemotingConfiguration.RegisterWellKnownServiceType()来实现:

1

2

3

RemotingConfiguration.RegisterWellKnownServiceType(

 typeof(ServerRemoteObject.ServerObject),

 "ServiceMessage",WellKnownObjectMode.SingleTon);

(2)SingleCall模式

注册对象的方法基本上和SingleTon模式相同,只需要将枚举参数WellKnownObjectMode改为SingleCall就可以了。

1

2

3

RemotingConfiguration.RegisterWellKnownServiceType(

 typeof(ServerRemoteObject.ServerObject),

 "ServiceMessage",WellKnownObjectMode.SingleCall);

(3)客户端激活模式

对于客户端激活模式,使用的方法又有不同,但区别不大,看了代码就一目了然。

1

2

3

RemotingConfiguration.ApplicationName = "ServiceMessage";

RemotingConfiguration.RegisterActivatedServiceType(

 typeof(ServerRemoteObject.ServerObject));

  为什么要在注册对象方法前设置ApplicationName属性呢?其实这个属性就是该对象的URI。对于WellKnown模式,URI是放在RegisterWellKnownServiceType()方法的参数中,当然也可以拿出来专门对ApplicationName属性赋值。而RegisterActivatedServiceType()方法的重载中,没有ApplicationName的参数,所以必须分开。

3、注销通道

  如果要关闭Remoting的服务,则需要注销通道,也可以关闭对通道的监听。在Remoting中当我们注册通道的时候,就自动开启了通道的监听。而如果关闭了对通道的监听,则该通道就无法接受客户端的请求,但通道仍然存在,如果你想再一次注册该通道,会抛出异常。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

IChannel[] channels = ChannelServices.RegisteredChannels;

foreach (IChannel eachChannel in channels)

{

 if (eachChannel.ChannelName == "MyTcp")

 {

 TcpChannel tcpChannel = (TcpChannel)eachChannel;

 tcpChannel.StopListening(null);

 ChannelServices.UnregisterChannel(tcpChannel);

 }

}

  代码中,RegisterdChannel属性获得的是当前已注册的通道。在Remoting中,是允许同时注册多个通道的,这一点会在后面说明。

四、客户端

  客户端主要做两件事,一是注册通道。这一点从图一就可以看出,Remoting中服务器端和客户端都必须通过通道来传递消息,以获得远程对象。第二步则是获得该远程对象。

1、注册通道:

1

2

TcpChannel channel = new TcpChannel();

ChannelServices.RegisterChannel(channel);

  注意在客户端实例化通道时,是调用的默认构造函数,即没有传递端口号。事实上,这个端口号是缺一不可的,只不过它的指定被放在后面作为了Uri的一部分。

2、获得远程对象。

  与服务器端相同,不同的激活模式决定了客户端的实现方式也将不同。不过这个区别仅仅是WellKnown激活模式和客户端激活模式之间的区别,而对于SingleTon和SingleCall模式,客户端的实现完全相同。

(1) WellKnown激活模式

  要获得服务器端的知名远程对象,可通过Activator进程的GetObject()方法来获得:

1

2

ServerRemoteObject.ServerObject serverObj = (ServerRemoteObject.ServerObject)Activator.GetObject(

 typeof(ServerRemoteObject.ServerObject), "tcp://localhost:8080/ServiceMessage");

  首先以WellKnown模式激活,客户端获得对象的方法是使用GetObject()。其中参数第一个是远程对象的类型。第二个参数就是服务器端的uri。如果是http通道,自然是用http://localhost:8080/ServiceMessage了。因为我是用本地机,所以这里是localhost,你可以用具体的服务器IP地址来代替它。端口必须和服务器端的端口一致。后面则是服务器定义的远程对象服务名,即ApplicationName属性的内容。

(2) 客户端激活模式

  如前所述,WellKnown模式在客户端创建对象时,只能调用默认的构造函数,上面的代码就说明了这一点,因为GetObject()方法不能传递构造函数的参数。而客户端激活模式则可以通过自定义的构造函数来创建远程对象。

客户端激活模式有两种方法:
1) 调用RemotingConfiguration的静态方法RegisterActivatedClientType()。这个方法返回值为Void,它只是将远程对象注册在客户端而已。具体的实例化还需要调用对象类的构造函数。

1

2

3

4

RemotingConfiguration.RegisterActivatedClientType(   

 typeof(ServerRemoteObject.ServerObject),

 "tcp://localhost:8080/ServiceMessage");

ServerRemoteObject.ServerObject serverObj = new ServerRemoteObject.ServerObject();

2) 调用进程Activator的CreateInstance()方法。这个方法将创建方法参数指定类型的类对象。它与前面的GetObject()不同的是,它要在客户端调用构造函数,而GetObject()只是获得对象,而创建实例是在服务器端完成的。CreateInstance()方法有很多个重载,我着重说一下其中常用的两个。

a、 public static object CreateInstance(Type type, object[] args, object[] activationAttributes);

参数说明:
type:要创建的对象的类型。
args :与要调用构造函数的参数数量、顺序和类型匹配的参数数组。如果 args 为空数组或空引用(Visual Basic 中为 Nothing),则调用不带任何参数的构造函数(默认构造函数)。
activationAttributes :包含一个或多个可以参与激活的属性的数组。

  这里的参数args是一个object[]数组类型。它可以传递要创建对象的构造函数中的参数。从这里其实可以得到一个结论:WellKnown激活模式所传递的远程对象类,只能使用默认的构造函数;而Activated模式则可以用户自定义构造函数。activationAttributes参数在这个方法中通常用来传递服务器的url。
假设我们的远程对象类ServerObject有个构造函数:

1

2

3

4

5

6

ServerObject(string pName,string pSex,int pAge)

{

 name = pName;

 sex = pSex;

 age = pAge;

}

那么实现的代码是:

1

2

3

4

5

6

7

object[] attrs = {new UrlAttribute("tcp://localhost:8080/ServiceMessage")};

object[] objs = new object[3];

objs[0] = "wayfarer";

objs[1] = "male";

objs[2] = 28;

ServerRemoteObject.ServerObject = Activator.CreateInstance(

 typeof(ServerRemoteObject.ServerObject),objs,attrs);

可以看到,objs[]数组传递的就是构造函数的参数。

b、public static ObjectHandle CreateInstance(string assemblyName, string typeName, object[] activationAttribute);

参数说明:
assemblyName :将在其中查找名为 typeName 的类型的程序集的名称。如果 assemblyName 为空引用(Visual Basic 中为 Nothing),则搜索正在执行的程序集。
typeName:首选类型的名称。
activationAttributes :包含一个或多个可以参与激活的属性的数组。

参数说明一目了然。注意这个方法返回值为ObjectHandle类型,因此代码与前不同:

1

2

3

4

object[] attrs = {new UrlAttribute("tcp://localhost:8080/EchoMessage")};  

ObjectHandle handle = Activator.CreateInstance("ServerRemoteObject",

 "ServerRemoteObject.ServerObject",attrs);

ServerRemoteObject.ServerObject obj = (ServerRemoteObject.ServerObject)handle.Unwrap();

这个方法实际上是调用的默认构造函数。ObjectHandle.Unwrap()方法是返回被包装的对象。

说明:要使用UrlAttribute,还需要在命名空间中添加:using System.Runtime.Remoting.Activation;

五、Remoting基础的补充

  通过上面的描述,基本上已经完成了一个最简单的Remoting程序。这是一个标准的创建Remoting程序的方法,但在实际开发过程中,我们遇到的情况也许千奇百怪,如果只掌握一种所谓的“标准”,就妄想可以“一招鲜、吃遍天”,是不可能的。

1、注册多个通道

  在Remoting中,允许同时创建多个通道,即根据不同的端口创建不同的通道。但是,Remoting要求通道的名字必须不同,因为它要用来作为通道的唯一标识符。虽然IChannel有ChannelName属性,但这个属性是只读的。因此前面所述的创建通道的方法无法实现同时注册多个通道的要求。

这个时候,我们必须用到System.Collection中的IDictionary接口:

注册Tcp通道:

1

2

3

4

5

6

7

IDictionary tcpProp = new Hashtable();

tcpProp["name"] = "tcp9090";

tcpProp["port"] = 9090;

IChannel channel = new TcpChannel(tcpProp,

 new BinaryClientFormatterSinkProvider(),

 new BinaryServerFormatterSinkProvider());

ChannelServices.RegisterChannel(channel);

注册Http通道:

1

2

3

4

5

6

7

IDictionary httpProp = new Hashtable();

httpProp["name"] = "http8080";

httpProp["port"] = 8080;

IChannel channel = new HttpChannel(httpProp,

 new SoapClientFormatterSinkProvider(),

 new SoapServerFormatterSinkProvider());

ChannelServices.RegisterChannel(channel);

在name属性中,定义不同的通道名称就可以了。

2、远程对象元数据相关性

  由于服务器端和客户端都要用到远程对象,通常的方式是生成两份完全相同的对象Dll,分别添加引用。不过为了代码的安全性,且降低客户端对远程对象元数据的相关性,我们有必要对这种方式进行改动。即在服务器端实现远程对象,而在客户端则删除这些实现的元数据。

由于激活模式的不同,在客户端创建对象的方法也不同,所以要分离元数据的相关性,也应分为两种情况。

(1) WellKnown激活模式:

通过接口来实现。在服务器端,提供接口和具体类的实现,而在客户端仅提供接口:

1

2

3

4

5

6

7

public interface IServerObject

{

 Person GetPersonInfo(string name,string sex,int age);

}

public class ServerObject:MarshalByRefObject,IServerObject

{ ......}

注意:两边生成该对象程序集的名字必须相同,严格地说,是命名空间的名字必须相同。

           (2) 客户端激活模式:

  如前所述,对于客户端激活模式,不管是使用静态方法,还是使用CreateInstance()方法,都必须在客户端调用构造函数实例化对象。所以,在客户端我们提供的远程对象,就不能只提供接口,而没有类的实现。实际上,要做到与远程对象元数据的分离,可以由两种方法供选择:

a、利用WellKnown激活模式模拟客户端激活模式:

方法是利用设计模式中的“抽象工厂”,下面的类图表描述了总体解决方案:

//img.jbzj.com/file_images/article/201605/201653092501685.gif

我们在服务器端的远程对象中加上抽象工厂的接口和实现类:

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

public interface IServerObject

{

 Person GetPersonInfo(string name,string sex,int age);

}

public interface IServerObjFactory

{

 IServerObject CreateInstance(); 

}

public class ServerObject:MarshalByRefObject,IServerObject

{

 public Person GetPersonInfo(string name,string sex,int age)

 {

  Person person = new Person();

  person.Name = name;

  person.Sex = sex;

  person.Age = age;

  return person;

 

}

public class ServerObjFactory:MarshalByRefObject,IServerObjFactory

{

 public IServerObject CreateInstance()

 {

  return new ServerObject();

 }

}

然后再客户端的远程对象中只提供工厂接口和原来的对象接口:

1

2

3

4

5

6

7

8

9

public interface IServerObject

{

 Person GetPersonInfo(string name,string sex,int age);

}

public interface IServerObjFactory

{

 IServerObject CreateInstance(); 

}

我们用WellKnown激活模式注册远程对象,在服务器端:

1

2

3

4

RemotingConfiguration.RegisterWellKnownServiceType(

 typeof(ServerRemoteObject.ServerObjFactory),

 "ServiceMessage",WellKnownObjectMode.SingleCall);

注意这里注册的不是ServerObject类对象,而是ServerObjFactory类对象。

客户端:

1

2

3

4

5

6

ServerRemoteObject.IServerObjFactory serverFactory =   

 (ServerRemoteObject.IServerObjFactory) Activator.GetObject(

 typeof(ServerRemoteObject.IServerObjFactory),

 "tcp://localhost:8080/ServiceMessage");

ServerRemoteObject.IServerObject serverObj = serverFactory.CreateInstance();

  为什么说这是一种客户端激活模式的模拟呢?从激活的方法来看,我们是使用了SingleCall模式来激活对象,但此时激活的并非我们要传递的远程对象,而是工厂对象。如果客户端要创建远程对象,还应该通过工厂对象的CreateInstance()方法来获得。而这个方法正是在客户端调用的。因此它的实现方式就等同于客户端激活模式。

b、利用替代类来取代远程对象的元数据

  实际上,我们可以用一个trick,来欺骗Remoting。这里所说的替代类就是这个trick了。既然是提供服务,Remoting传递的远程对象其实现的细节当然是放在服务器端。而要在客户端放对象的副本,不过是因为客户端必须调用构造函数,而采取的无奈之举。既然具体的实现是在服务器端,又为了能在客户端实例化,那么在客户端就实现这些好了。至于实现的细节,就不用管了。

  如果远程对象有方法,服务器端则提供方法实现,而客户端就提供这个方法就OK了,至于里面的实现,你可以是抛出一个异常,或者return 一个null值;如果方法返回void,那么里面可以是空。关键是这个客户端类对象要有这个方法。这个方法的实现,其实和方法的声明差不多,所以我说是一个trick。方法如是,构造函数也如此。

还是用代码来说明这种“阴谋”,更直观:

服务器端:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public class ServerObject:MarshalByRefObject

{

 public ServerObject()

 {

 }

 public Person GetPersonInfo(string name,string sex,int age)

 {

  Person person = new Person();

  person.Name = name;

  person.Sex = sex;

  person.Age = age;

  return person;

 

}

客户端:

1

2

3

4

5

6

7

8

9

10

11

12

public class ServerObject:MarshalByRefObject

{

 public ServerObj()

 {

  throw new System.NotImplementedException();

 }

 public Person GetPersonInfo(string name,string sex,int age)

 {

  throw new System.NotImplementedException();

 

}

  比较客户端和服务器端,客户端的方法GetPersonInfo(),没有具体的实现细节,只是抛出了一个异常。或者直接写上语句return null,照样OK。我们称客户端的这个类为远程对象的替代类。

3、利用配置文件实现

  前面所述的方法,于服务器uri、端口、以及激活模式的设置是用代码来完成的。其实我们也可以用配置文件来设置。这样做有个好处,因为这个配置文件是Xml文档。如果需要改变端口或其他,我们就不需要修改程序,并重新编译,而是只需要改变这个配置文件即可。

(1) 服务器端的配置文件:

1

2

3

4

5

6

7

8

9

10

11

12

<configuration>

 <system.runtime.remoting>

 <application name="ServerRemoting">

  <service>

  <wellknown mode="Singleton" type="ServerRemoteObject.ServerObject" objectUri="ServiceMessage"/>

  </service>

  <channels>

   <channel ref="tcp" port="8080"/>

  </channels>

 </application>

 </system.runtime.remoting>

</configuration>

如果是客户端激活模式,则把wellknown改为activated,同时删除mode属性。

把该配置文件放到服务器程序的应用程序文件夹中,命名为ServerRemoting.config。那么前面的服务器端程序直接用这条语句即可:

1

RemotingConfiguration.Configure("ServerRemoting.config");

(2) 客户端配置文件

如果是客户端激活模式,修改和上面一样。调用也是使用RemotingConfiguration.Configure()方法来调用存储在客户端的配置文件。

配置文件还可以放在machine.config中。如果客户端程序是web应用程序,则可以放在web.config中。

4、启动/关闭指定远程对象

  Remoting中没有提供类似UnregisterWellKnownServiceType()的方法,也即是说,一旦通过注册了远程对象,如果没有关闭通道的话,该对象就一直存在于通道中。只要客户端激活该对象,就会创建对象实例。如果Remoting传送的只有一个远程对象,这不存在问题,关闭通道就可以了。如果传送多个远程对象呢?要关闭指定的远程对象应该怎么做?关闭之后又需要启动又该如何?

  我们注意到在Remoting中提供了Marshal()和Disconnect()方法,答案就在这里。Marshal()方法是将MarshalByRefObject类对象转化为ObjRef类对象,这个对象是存储生成代理以与远程对象通讯所需的所有相关信息。这样就可以将该实例序列化以便在应用程序域之间以及通过网络进行传输,客户端就可以调用了。而Disconnect()方法则将具体的实例对象从通道中断开。

方法如下:
首先注册通道:

1

2

TcpChannel channel = new TcpChannel(8080);

ChannelServices.RegisterChannel(channel);

接着启动服务:
先在服务器端实例化远程对象。

ServerObject obj = new ServerObject();

然后,注册该对象。注意这里不用RemotingConfiguration.RegisterWellKnownServiceType(),而是使用RemotingServices.Marshal():

ObjRef objrefWellKnown = RemotingServices.Marshal(obj, “ServiceMessage”);

如果要注销对象,则:

RemotingServices.Disconnect(obj);

  要注意,这里Disconnect的类对象必须是前面实例化的对象。正因为此,我们可以根据需要创建指定的远程对象,而关闭时,则Disconnect之前实例化的对象。

  至于客户端的调用,和前面WellKnown模式的方法相同,仍然是通过Activator.GetObject()来获得。但从实现代码来看,我们会注意到一个问题,由于服务器端是显式的实例化了远程对象,因此不管客户端有多少,是否相同,它们调用的都是同一个远程对象。因此我们将这个方法称为模拟的SingleTon模式。

客户端激活模式

  我们也可以通过Marshal()和Disconnect()来模拟客户端激活模式。首先我们来回顾“远程对象元数据相关性”一节,在这一节中,我说到采用设计模式的“抽象工厂”来创建对象实例,以此用SingleCall模式来模拟客户端激活模式。在仔细想想前面的模拟的SingleTon模式。是不是答案就将呼之欲出呢?

  在“模拟的SingleTon”模式中,我们是将具体的远程对象实例进行Marshal,以此让客户端获得该对象的引用信息。那么我们换一种思路,当我们用抽象工厂提供接口,工厂类实现创建远程对象的方法。然后我们在服务器端创建工厂类实例。再将这个工厂类实例进行Marshal。而客户端获取对象时,不是获取具体的远程对象,而是获取具体的工厂类对象。然后再调用CreateInstance()方法来创建具体的远程对象实例。此时,对于多个客户端而言,调用的是同一个工厂类对象;然而远程对象是在各个客户端自己创建的,因此对于远程对象而言,则是由客户端激活,创建的是不同对象了。

当我们要启动/关闭指定对象时,只需要用Disconnet()方法来注销工厂类对象就可以了。

六、小结

  Microsoft.Net Remoting真可以说是博大精深。整个Remoting的内容不是我这一篇小文所能尽述的,更不是我这个Remoting的初学者所能掌握的。王国维在《人间词话》一书中写到:古今之成大事业大学问者,必经过三种境界。“昨夜西风凋碧树,独上高楼,望尽天涯路。”此第一境界也。“衣带渐宽终不悔,为伊消得人憔悴。”此第二境界也。“众里寻他千百度,蓦然回首,那人却在灯火阑珊处。”此第三境界也。如以此来形容我对Remoting的学习,还处于“独上高楼,望尽天涯路”的时候,真可以说还未曾登堂入室。

  或许需得“衣带渐宽”,学得Remoting“终不悔”,方才可以“蓦然回首”吧。

  以上就是.Net Remoting基础的全部内容,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章: