乐者为王

Do one thing, and do it well.

为什么要软件建模

模型是对现实的简化。需要进行建模的原因有以下几点:

  1. 人们对复杂问题的理解能力是有限的;
  2. 我们要开发的软件系统是复杂的;
  3. 对系统的完整的理解有助于正确地实施工作(但不一定能使开发工作进展的更快);
  4. 我们不能完整的理解一个复杂的系统,为了能够更好地理解正在开发的系统,因此要对它进行建模。

那么,是不是任何情况下都要建模呢?未必尽然。是否要进行软件建模是由系统的复杂程度决定的。如果系统的复杂性在我们的控制范围之内,那么就不需要建模。不过,有一个自然趋势:随着时间的推移,几乎所有的应用系统变得越来越复杂。因此要记住的是:狗窝总有一天会膨胀成大厦,并且因为不堪承受其自身的重量而倒塌。

贫血领域模型

英文原文:https://martinfowler.com/bliki/AnemicDomainModel.html

贫血领域模型是那些已近存在了相当长时间的反模式中的一份子,然而目前似乎有一个特定的高潮。我和Eric Evans聊过这个,并且我们都注意到它们似乎越来越流行。作为真正的领域模型支持者,这不是一件好事。

贫血领域模型的基本症状是乍一看它就像真的领域模型一样。这些对象大多以领域空间中的名词命名,并且它们被真正的领域模型拥有的丰富的关系和结构连接。当你观察行为时问题来了,你意识到这些对象几乎没有任何行为,仅仅只是封装了getter和setter方法。这些模型的设计规则往往说不要把任何领域逻辑放到领域对象里。取而代之的是一组服务对象捕获了所有的领域逻辑。这些在领域对象上层的服务把领域对象当数据使用。

这种反模式最根本的恐怖在于它和面向对象设计的基本理念完全相反,它只是把数据和处理结合在一起。贫血领域模型其实只是一种过程式风格的设计,正是那种像我(和Eric)自Smalltalk早期以来一直在与之作斗争的顽固者。更糟糕的是,许多人认为贫血对象是真正的对象,因此完全没有抓住面向对象设计是怎么回事的要领。

现在面向对象的纯粹主义当然很好,但我意识到我需要更多的基本论据来反对这种贫血症。本质上贫血领域模型的问题是它们承担了领域模型所有的成本,没有产生任何的好处。主要成本是映射到数据库的笨拙,它通常会导致一整层的O/R映射。在而且只有在你使用强大的面向对象技术去组织复杂的逻辑时这才值得。通过把所有的行为拉取到服务中,不管用什么方法,基本上你最后得到的都是事务脚本,从而失去领域模型能带来的优势。正如我在《企业应用架构模式》中讨论过的,领域模型并不总是最好的工具。

值得强调的是,把行为放进领域对象不应违背利用分层将领域逻辑从诸如持久和表现责任中分离的坚实方法。在领域对象中的逻辑应该是领域逻辑——验证、计算、业务规则——无论你喜欢叫它什么。(某些情况下,你会产生把数据源和表现逻辑放到领域对象里的争论,但它和我对贫血症的看法无关。)

这一切混乱的来源是许多OO专家强烈推荐在领域模型顶部放一层过程式服务,来形成一个服务层。但这不是让领域模型完全没有行为的一个论据,其实服务层主张服务层要和行为丰富的领域模型一起使用。

Eric Evans的精彩书籍《领域驱动设计》有如下的文字谈及这些层:

应用层【他对服务层的命名】:定义软件可以完成的工作,并且指挥具有丰富含义的领域对象来解决问题。这个层所负责的任务对业务影响深远,对跟其它系统的应用层进行交互非常必要。这个层要保持简练。它不包括处理业务规则或知识,只是给下一层中相互协作的领域对象协调任务、委托工作。在这个层次中不反映业务情况的状态,但反映用户或程序的任务进度的状态。

领域层(或者模型层):负责表示业务概念、业务状况的信息以及业务规则。并且保持这些内容的技术细节由基础结构层来完成,反映业务状况的状态在该层中被控制和使用。这一层是业务软件的核心。

这里的关键点是服务层是瘦的——所有的关键逻辑存在于领域层。他在服务模式中重申了这个观点:

现在,最常犯的错误就是太轻易地放弃把这种行为配置到合适的对象上,逐渐地滑向过程式编程。

我不知道为什么这个反模式如此常见。我猜想它是因为大多数人还没有真正地和正确的领域模型工作过,特别是如果他们有数据背景。有些技术鼓励正确的领域模型,例如J2EE的实体Bean,它是我偏爱POJO领域模型的理由之一。

总之,你在服务中发现越多的行为,你就越难体会到领域模型的好处。如果你所有的逻辑都在服务中,那你将得不到任何好处。

练习破解Andrnalin的Crackme

拿到这个程序,用PEiD查看这个程序,发现是用Microsoft Visual Basic 5.0编写的且没有加过壳,所以用W32Dasm反汇编。通过Functions/Imports查看它调用的函数,能够看见MSVBVM50!__vbaStrCmp字样。

  1. 启动SoftICE,然后运行Crackme程序;
  2. 在文本框中输入12345678;
  3. Ctrl+D来到SoftICE中,输入bpx __vbaStrCmp,按回车后用Ctrl+D命令返回Crackme;
  4. 点击OK,程序被SoftICE中断;
  5. 按F12回到调用__vbaStrCmp的地方;
  6. 按F6切换到代码窗口,移动光标,直到代码窗口中出现如下程序段:
1
2
3
4
001B:00401D70                MOV ECX, [EBP-28]
001B:00401D73                PUSH ECX
001B:00401D74                PUSH 00401A54
001B:00401D79                CALL [MSVBVM50!__vbaStrCmp]

因此可以知道__vbaStrCmp比较的是ECX和00401A54所指向的字符串。记下00401A54这个值,然后重复步骤1、2、3、4。接着查看ECX和00401a54指向的内容就可以知道正确的key了。

d ecx的显示如下:

1
2
0023:0013CC1C 31 00 32 00 33 00 34 00-35 00 36 00 37 00 38 00 1.2.3.4.5.6.7.8.
0023:0013CC2C 00 00 72 00 61 00 6D 00-46 00 69 00 79 00 05 00 ..r.a.m.F.i.y...

d 401a54的显示如下:

1
2
0023:00401A54 53 00 79 00 6E 00 54 00-61 00 58 00 20 00 32 00 S.y.n.T.a.X. .2.
0023:00401A64 6F 00 6F 00 31 00 00 00-4C 00 00 00 52 00 69 00 o.o.1...L...R.i.

12345678是我们输入的key,所以正确的key是SynTaX 2oo1(注意中间的空格)。

注意:如果你是在破解VB6程序,你应该在断点前加上msvbvm60!。

使用Win32实现系统托盘程序

废话不多说,直接上代码:

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#include <windows.h>
#include "resource.h"

static LPCTSTR szAppName = TEXT("TrayHelper");
static LPCTSTR szCaption = TEXT("TrayIcon");

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HMENU hMenu;
    static UINT WM_TASKBARCREATED;

    POINT point;
    HINSTANCE hInstance;
    NOTIFYICONDATA nid;

    switch (message)
    {
    case WM_CREATE:
        // 不要修改TaskbarCreated字符串,这是系统任务栏自定义的消息
        WM_TASKBARCREATED = RegisterWindowMessage(TEXT("TaskbarCreated"));

        hInstance = ((LPCREATESTRUCT)lParam)->hInstance;

        hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_TRAYMENU));
        hMenu = GetSubMenu(hMenu, 0);

        nid.cbSize = sizeof(nid);
        nid.hWnd = hWnd;
        nid.uID = IDI_PIRAMIDE;
        nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
        nid.uCallbackMessage = WM_USER;
        nid.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_PIRAMIDE));
        strcpy(nid.szTip, szAppName);
        Shell_NotifyIcon(NIM_ADD, &nid);
        break;

    case WM_USER:
        if (lParam == WM_RBUTTONDOWN)
        {
            GetCursorPos(&point);
            // 处理当用户按下ESCAPE键或者在菜单之外单击鼠标时菜单不会消失的情况
            SetForegroundWindow(hWnd);
            TrackPopupMenu(hMenu, TPM_RIGHTBUTTON, point.x, point.y, 0, hWnd, NULL);
        }
        break;

    case WM_COMMAND:
        switch (LOWORD(wParam)) {
        case IDM_TRAYSETTINGS:
            MessageBox(hWnd, TEXT("Settings not yet implemented!"),
                             szAppName, MB_ICONEXCLAMATION | MB_OK);
            return 0;

        case IDM_TRAYHELP:
            MessageBox(hWnd, TEXT("Help not yet implemented!"),
                             szAppName, MB_ICONEXCLAMATION | MB_OK);
            return 0;

        case IDM_TRAYABOUT:
            MessageBox(hWnd, TEXT("About not yet implemented!"),
                             szAppName, MB_ICONEXCLAMATION | MB_OK);
            return 0;

        case IDM_TRAYEXIT:
            SendMessage(hWnd, WM_CLOSE, 0, 0);
            return 0;
        }
        break;

    case WM_DESTROY:
        // 处理点击Exit菜单退出后图标仍在托盘区显示,要把鼠标在图标上面过一下才会消失的情况
        nid.uID = IDI_PIRAMIDE;
        nid.hWnd = hWnd;
        Shell_NotifyIcon(NIM_DELETE, &nid);
        PostQuitMessage(0);
        break;

    default:
        /*
         * 防止当Explorer.exe崩溃以后,程序在系统托盘中的图标就消失了。
         *
         * 原理:Explorer.exe重新载入后会重建系统任务栏。当系统任务栏建立的时候会向系统内所有
         * 注册接收TaskbarCreated消息的顶级窗口发送一条消息,我们只需要捕捉这个消息,并重建系
         * 统托盘的图标即可。
         */
        if (message == WM_TASKBARCREATED)
        {
            SendMessage(hWnd, WM_CREATE, wParam, lParam);
        }
        break;
    }
    return DefWindowProc(hWnd, message, wParam, lParam);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                                        LPSTR szCmdLine, int iCmdShow)
{
    HWND hWnd;
    MSG msg;
    WNDCLASS wc;

    HWND handle = FindWindow(NULL, szCaption);
    if (handle != NULL)
    {
        MessageBox(NULL, TEXT("Application is already running"),
                         szAppName, MB_ICONERROR);
        return 0;
    }

    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = WndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName = NULL;
    wc.lpszClassName = szAppName;

    if (!RegisterClass(&wc))
    {
        MessageBox(NULL, TEXT("This program requires Windows NT!"),
                         szAppName, MB_ICONERROR);
        return 0;
    }

    // 此处使用WS_EX_TOOLWINDOW属性来隐藏显示在任务栏上的窗口程序按钮
    hWnd = CreateWindowEx(WS_EX_TOOLWINDOW,
                        szAppName, szCaption,
                        WS_POPUP,
                        CW_USEDEFAULT,
                        CW_USEDEFAULT,
                        CW_USEDEFAULT,
                        CW_USEDEFAULT,
                        NULL, NULL, hInstance, NULL);

    ShowWindow(hWnd, iCmdShow);
    UpdateWindow(hWnd);

    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}

练习破解Brad Soblesky的Crackme

Crackme程序

这次破解参考了wind[CCG]写的《Crackme-Brad Soblesky的Crackme程序破解》文章。因为我用的是Windows 2000操作系统,TRW2000不能在上面使用,所以我用SoftICE 4.05 来破解。

用PEiD查看这个程序,发现是用VC++ 6.0编写的且没加过壳,所以用W32Dasm反汇编。打开Functions/Imports查看它调用的函数,能够看见KERNEL32.lstrcmpA。双击它可以看到如下程序段:

1
2
3
4
5
6
7
8
9
10
11
:00401585 8d4de4 lea ecx, dword ptr [ebp-1c]
:00401588 51 push ecx
:00401589 8d55f4 lea edx, dword ptr [ebp-0c]
:0040158c 52 push edx

* reference to: kernel32.lstrcmpa, ord:02fch
:0040158d ff1500204000 call dword ptr [00402000]
:00401593 85c0 test eax, eax                      -> 典型的判断,肯定是这里了

:00401595 7516 jne 004015ad                       -> 看见这个跳转了吗
:00401597 6a40 push 00000040
  1. 启动SoftICE,然后运行Crackme程序;
  2. 在文本框中输入78787878;
  3. Ctrl+D 来到SoftICE中,输入bpx lstrcmpA,按回车后用Ctrl+D命令返回Crackme;
  4. 按check,程序中断;
  5. 按F11回到调用lstrcmpA的地方,发现CS寄存器的值为001b,输入bpx 001b: 00401585,按回车后用Ctrl+D命令返回Crackme;
  6. 按check,程序中断;
  7. F10来到如下程序段:
1
2
3
4
:00401585 8d4de4 lea ecx, dword ptr [ebp-1c]
:00401588 51 push ecx                             -> 在此处用d ecx就可以看到注册码了
:00401589 8d55f4 lea edx, dword ptr [ebp-0c]
:0040158c 52 push edx

注册码:<BrD-SoB>

SoftICE 4.05入门指南

首先,在VC++ 6.0中新建工程Hello,建立源文件Hello.c,编译链接成可执行的Hello.exe文件。

1
2
3
4
5
6
7
#include <windows.h>

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
    MessageBox(NULL, TEXT("Hello, world!"), TEXT("Crack"), 0);
    return 0;
}

然后安装SoftICE 4.05 for Windows 2000,选择manual启动方式。在菜单中选择Start SoftICE快捷方式启动SoftICE。

接着双击Hello.exe程序,按Ctrl+D呼叫出SoftICE,输入bpx MessageBox后按回车键(记住:一定要按回车键啊)。如果提示“Symbol not Defined (xxx)”,可以打开WINNT\system32\winice.dat文件,将所有DLL前的分号去除,然后重新启动SoftICE并且重复上述步骤。

最后按Ctrl+D返回,双击Hello.exe文件,此时就会发现设置的断点被SoftICE中断住了。

Servlet.init()中Log4j不能输出日志

web.xml配置文件:

1
2
3
4
5
6
7
8
9
10
<web-app>
    <servlet>
        <servlet-name>hello</servlet-name>
        <servlet-class>com.example.learning.HelloServlet</servlet-class>
        <init-param>
            <param-name>log4jConfig</param-name>
            <param-value>/WEB-INF/log4j.properties</param-value>
        </init-param>
    </servlet>
</web-app>

log4j.properties配置文件:

1
2
3
4
log4j.rootLogger=ALL, Console
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=[%t] %37c %3x - %m%n

HelloServlet.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
33
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;

public class HelloServlet extends HttpServlet {
    private Logger logger = Logger.getLogger(HelloServlet.class);

    public void init() throws ServletException {
        super.init();

        ServletConfig config = getServletConfig();
        ServletContext sc = getServletContext();

        String log4jConfig = config.getInitParameter("log4jConfig");
        System.out.println(log4jConfig);

        if (log4jConfig == null) {
            BasicConfigurator.configure();
        } else {
            PropertyConfigurator.configure(sc.getRealPath(log4jConfig));
        }

        logger.info("This is an info message!");
    }

    public void destroy() {
    }
}

程序运行没有问题,可就是不能在Console视图中输出日志信息,而且使用System.out.println()也不能输出,不过在把private Logger logger行注释掉后System.out.println()就可以输出了。查看TOMCAT_HOME/logs中的日志文件,发现有如下异常:

1
2
3
----- Root Cause -----
java.lang.NoClassDefFoundError: org/apache/log4j/Logger
  at com.example.learning.HelloServlet.<init>(HelloServlet.java:15)

找不到Logger类?是不是WEB-INF/lib目录下没有log4j.jar呢?转到WEB-INF/lib目录,果然没有发现log4j.jar文件。把log4j.jar拷过去再试一下,真的OK了。

JPetStore 4.0.5配置MySQL数据库时遇到的问题

在配置JPetStore 4.0.5时出了一点问题,写此文章记录下解决方法,以便日后查找。

按照教程配置好后运行Tomcat,在点击“Enter the Store”时出现了HTTP 500错误。从日志中的异常记录来看是没有找到MySQL的驱动程序。

1
SimpleDataSource: Error while loading properties. Cause: java.lang.ClassNotFoundException: driver

可是我已经把驱动程序包放到WEB-INF/lib目录下了呀!

查看sql-xml-config.xml文件中dataSource的属性,发现它们的值如下:

1
2
3
4
5
6
7
8
9
10
11
<transactionManager type="JDBC">
    <dataSource type="SIMPLE">
        <property value="${driver}" name="JDBC.Driver" />
        <property value="${url}" name="JDBC.ConnectionURL" />
        <property value="${username}" name="JDBC.Username" />
        <property value="${password}" name="JDBC.Password" />
        <property value="15" name="Pool.MaximumActiveConnections" />
        <property value="15" name="Pool.MaximumIdleConnections" />
        <property value="1000" name="Pool.MaximumWait" />
      </dataSource>
</transactionManager>

而在database.properties中的值分别是:

1
2
3
4
SimpleDriver=org.gjt.mm.mysql.Driver
SimpleUrl=jdbc:mysql://localhost:3306/jpetstore
SimpleUsername=jpetstore
SimplePassword=ibatis

因为dataSource中的属性值是从database.properties文件中读取的,所以要将database.properties中的属性名改成和sql-xml-config.xml中的一致。这样就不会再出现找不到驱动的异常了。改写后的database.properties内容如下:

1
2
3
4
driver=org.gjt.mm.mysql.Driver
url=jdbc:mysql://localhost:3306/jpetstore
username=jpetstore
password=ibatis

Eclipse中Ant执行JUnit测试的问题(2)

Eclipse 3.0.3 + Ant 1.6.2 + JUnit 3.8.1

构建配置文件build.xml:

1
2
3
4
5
6
7
8
<target name="junit">
    <junit>
        <formatter type="xml" />
        <batchtest todir="${report.dir}">
            <fileset dir="${classes.dir}" includes="**/*Test.class" />
        </batchtest>
    </junit>
</target>

执行任务的时候出现了异常:

1
2
3
4
5
6
7
8
9
10
java.lang.ClassNotFoundException: com.example.learning.HelloTest
    at java.net.URLClassLoader$1.run(Unknown Source)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at java.lang.ClassLoader.loadClassInternal(Unknown Source)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Unknown Source)

网上搜索没有找到答案,到Ant官网上发现 http://ant.apache.org/faq.html#delegating-classloader 这篇文章讲了该问题。只要在junit任务里加入classpath指出要加载类的路径就可以了。修改后的build.xml文件如下:

1
2
3
4
5
6
7
8
9
10
11
<target name="junit">
    <junit>
        <classpath>
            <pathelement location="${classes.dir}" />
        </classpath>
        <formatter type="xml" />
        <batchtest todir="${report.dir}">
            <fileset dir="${classes.dir}" includes="**/*Test.class" />
        </batchtest>
    </junit>
</target>