本篇讨论的问题是对项目中遇到的难题进行技术穿刺。
做过项目的人都知道,在构思完一个项目的功能之后,紧接着的事情就是考虑这些构思的功能如何实现,对于自己不熟悉的领域,要进行技术穿刺。我的穿刺方法为先查找有无比较好的开源组件可用,如果没有,就查找相关的文档,自己编写和测试代码。
在这一篇,我主要解决三个问题。
1、解决字符串加密的问题,在前面一篇中,我们设计用户模块的时候,准备将用户的密码字段以MD5加密的方式保存,因此,这里需要写一个对字符串加密生成MD5字符串的方法;
2、解决生成图像缩略图和生成验证码的问题;
3、解决url重写的问题,之所以要用到url重写,主要是为了让用户在访问自己的主页时,可以使用http://www.xkland.com/username或者http://username.xkland.com这样的形式,而不是像http://www.xkland.com/index.jsp?username=xxx这样难看的形式。
需要说明的是,要解决上面的三个问题,不是没有开源的东西可用,而是我觉得每次都要整合不同的组件是在是太麻烦,而我们需要的功能也不是很复杂,我们不需要太通用的东西,只要能够解决这里特定的问题就行了,因此不如自己动手实现,同时还可以获得技术上的提高。
首先来看看MD5加密的问题,JDK中本来提供有数据加密的支持,其中java.security.MessageDigest类就可以实现MD5的加密,但是,加密后生成的数据是byte[]类型的,这里只需要写一个方法将它转换为字符串就行,代码如下:
package
com.xkland.util;
import
java.security.MessageDigest;
import
java.lang.NullPointerException;
import
java.security.NoSuchAlgorithmException;
public
class
StringUtil
{
public
static
char
[] num_chars
=
new
char
[]
{
'
0
'
,
'
1
'
,
'
2
'
,
'
3
'
,
'
4
'
,
'
5
'
,
'
6
'
,
'
7
'
,
'
8
'
,
'
9
'
,
'
A
'
,
'
B
'
,
'
C
'
,
'
D
'
,
'
E
'
,
'
F
'
}
;
public
static
String toMD5String(String input)
throws
NullPointerException,NoSuchAlgorithmException
{
if
(input
==
null
)
throw
new
NullPointerException();
char
[] output
=
new
char
[
32
];
MessageDigest md
=
MessageDigest.getInstance(
"
MD5
"
);
byte
[] by
=
md.digest(input.getBytes());
for
(
int
i
=
0
;i
<
by.length;i
++
)
{
output[
2
*
i]
=
num_chars[ (by[i]
&
0xf0
)
>>
4
];
output[
2
*
i
+
1
]
=
num_chars[ by[i]
&
0xf
];
}
return
new
String(output);
}
}
下面是它的测试用例:
package
com.xkland.util;
import
junit.framework.TestCase;
public
class
StringUtilTest
extends
TestCase
{
public
void
testToMD5String()
{
try
{
System.out.println(StringUtil.toMD5String(
"
abc
"
));
}
catch
(Exception e)
{
}
}
}
运行测试用例,输出结果为:
900150983CD24FB0D6963F7D28E17F72
再来说说关于图像缩略图生成的问题,我准备将它设置为一个可以让Spring管理的类,简单的说,可以利用Spring的配置文件来设置该类的一些属性,比如原图像保存的目录和目标图像保存的目录,生成的缩略图的大小,生成缩略图的方式。这里特别需要说明的就是这个生成缩略图的方式,我们即可以指定它只简单的执行缩放,也可以指定它进行剪裁以后再缩放。为什么要这么设计,请大家看看如下的效果图,对于下面这两张美女图:
如果我们只通过简单的缩放来生成缩略图,那么在网页上的布局效果为:
如果我们通过先剪切后缩放的效果来生成缩略图,那么在网页上布局的效果为:
可以看到通过第二种方式生成的缩略图布局要漂亮一些,但是会损失图片的信息。因此,两种方式各有优劣。所以在设计的时候就设计为能够让用户灵活配置。
对于有些网友反映的gif动画经过缩放以后就不能动了,这个问题的主要原因是因为Java SDK 1.4和1.5版本的ImageIO类只能读gif格式的文件,而不能写gif格式的文件,因此,对于gif格式的文件,生成的缩略图只能用png格式代替,在我的设计中,我准备让bmp格式的文件也让png格式代替,因为png格式生成的文件更小,而且也不损失图片质量。至于Java SDK 1.4和1.5版不支持写gif格式的文件,可以查看Java文档,下面是截图:
最新推出的Java SDK 6是可以写gif格式的文件的,因此如果要解决这个问题,可以使用最新的JDK,下面是文档截图:
下面是我写的生成缩略图和生成验证码的ImageUtil类的源代码:
package
com.xkland.util;
import
javax.imageio.ImageIO;
import
java.awt.image.BufferedImage;
import
java.io.File;
import
java.awt.Image;
import
java.awt.Graphics2D;
import
java.util.Random;
import
java.awt.Font;
import
javax.servlet.http.HttpSession;
public
class
ImageUtil
{
private
String sourceDir;
//
图片的存放路径
private
String destinationDir;
//
缩略图的存放路径
private
String mode;
//
生成缩略图的模式,可选ScaleOnly或ClipAndScale
private
String width;
//
缩略图的宽度
private
String height;
//
缩略图的高度
private
String characterStorage;
//
用来生成验证码的字符仓库
//
以下代码段是为了使用Spring注入属性
public
void
setCharacterStorage(String characterStorage)
{
this
.characterStorage
=
characterStorage;
}
public
void
setDestinationDir(String destinationDir)
{
this
.destinationDir
=
destinationDir;
}
public
void
setHeight(String height)
{
this
.height
=
height;
}
public
void
setMode(String mode)
{
this
.mode
=
mode;
}
public
void
setSourceDir(String sourceDir)
{
this
.sourceDir
=
sourceDir;
}
public
void
setWidth(String width)
{
this
.width
=
width;
}
//
生成缩略图的方法,默认缩略图的文件名和原图相同,存放路径不同
public
void
createMicroImage(String fileName)
throws
Exception
{
//
判断sourceDir的格式是否为以"\"结尾,并生成完整的路径
String sourceFileName;
String destinationFileName;
if
(sourceDir.lastIndexOf(
'
\\
'
)
!=
(sourceDir.length()
-
1
))
{
sourceFileName
=
sourceDir
+
"
\\
"
+
fileName;
destinationFileName
=
destinationDir
+
"
\\
"
+
fileName;
}
else
{
sourceFileName
=
sourceDir
+
fileName;
destinationFileName
=
destinationDir
+
fileName;
}
//
创建文件,并判断原文件是否存在
File sourceFile
=
new
File(sourceFileName);
if
(
!
sourceFile.exists())
{
throw
new
Exception();
}
//
根据扩展名判断原文件的格式
String extension
=
fileName.substring(fileName.lastIndexOf(
'
.
'
)
+
1
);
if
(
!
extension.equalsIgnoreCase(
"
jpg
"
)
&&
!
extension.equalsIgnoreCase(
"
bmp
"
)
&&
!
extension.equalsIgnoreCase(
"
gif
"
)
&&
!
extension.equalsIgnoreCase(
"
png
"
))
{
throw
new
Exception();
}
//
判断缩略图的宽度和高度是否正确,如果不能正确解析则抛出异常
int
destinationWidth
=
Integer.parseInt(width);
int
destinationHeight
=
Integer.parseInt(height);
//
判断缩放模式是否正确,如果配置错误,则抛出异常
if
(
!
mode.equalsIgnoreCase(
"
ScaleOnly
"
)
&&
!
mode.equalsIgnoreCase(
"
ClipAndScale
"
))
{
throw
new
Exception();
}
//
读取图像文件,并创建BufferedImage对象,如果不能读取,则抛出异常
BufferedImage image
=
null
;
image
=
ImageIO.read(sourceFile);
if
(image
==
null
)
{
throw
new
Exception();
}
//
获取原图像文件的高度和宽度
int
sourceWidth
=
image.getWidth();
int
sourceHeight
=
image.getHeight();
//
生成缩略图
if
(mode.equalsIgnoreCase(
"
ScaleOnly
"
))
{
BufferedImage destinationImage;
if
((
float
)sourceWidth
/
destinationWidth
>
(
float
)sourceHeight
/
destinationHeight)
{
Image tempImage
=
image.getScaledInstance(destinationWidth, (
int
)(destinationWidth
*
((
float
)sourceHeight
/
sourceWidth)), Image.SCALE_DEFAULT);
destinationImage
=
new
BufferedImage(destinationWidth, (
int
)(destinationWidth
*
((
float
)sourceHeight
/
sourceWidth)),BufferedImage.TYPE_INT_RGB);
Graphics2D graphics
=
destinationImage.createGraphics();
graphics.drawImage(tempImage,
0
,
0
,
null
);
}
else
{
Image tempImage
=
image.getScaledInstance((
int
)(destinationHeight
*
((
float
)sourceWidth
/
sourceHeight)), destinationHeight, Image.SCALE_DEFAULT);
destinationImage
=
new
BufferedImage((
int
)(destinationHeight
*
((
float
)sourceWidth
/
sourceHeight)), destinationHeight,BufferedImage.TYPE_INT_RGB);
Graphics2D graphics
=
destinationImage.createGraphics();
graphics.drawImage(tempImage,
0
,
0
,
null
);
}
//
如果是bmp或者gif,则缩略图为png格式
if
(extension.equalsIgnoreCase(
"
bmp
"
)
||
extension.equalsIgnoreCase(
"
gif
"
))
{
extension
=
"
png
"
;
destinationFileName
=
destinationFileName.substring(
0
, destinationFileName.lastIndexOf(
'
.
'
))
+
"
.
"
+
extension;
}
File destinationFile
=
new
File(destinationFileName);
ImageIO.write(destinationImage, extension, destinationFile);
}
else
{
BufferedImage destinationImage;
if
((
float
)sourceWidth
/
destinationWidth
>
(
float
)sourceHeight
/
destinationHeight)
{
//
先裁减
int
x
=
sourceWidth
-
(
int
)(sourceHeight
*
((
float
)destinationWidth
/
destinationHeight));
Image clipedImage
=
image.getSubimage((
int
)(
0.5
*
x),
0
, (
int
)(sourceHeight
*
((
float
)destinationWidth
/
destinationHeight)), sourceHeight);
//
后缩放
Image scaledImage
=
clipedImage.getScaledInstance(destinationWidth, destinationHeight, Image.SCALE_DEFAULT);
destinationImage
=
new
BufferedImage(destinationWidth, destinationHeight,BufferedImage.TYPE_INT_RGB);
Graphics2D graphics
=
destinationImage.createGraphics();
graphics.drawImage(scaledImage,
0
,
0
,
null
);
}
else
{
//
先裁减
int
y
=
sourceHeight
-
(
int
)(sourceWidth
*
((
float
)destinationHeight
/
destinationWidth));
Image clipedImage
=
image.getSubimage(
0
, (
int
)(
0.5
*
y), sourceWidth, (
int
)(sourceWidth
*
((
float
)destinationHeight
/
destinationWidth)));
//
后缩放
Image scaledImage
=
clipedImage.getScaledInstance(destinationWidth, destinationHeight, Image.SCALE_DEFAULT);
destinationImage
=
new
BufferedImage(destinationWidth, destinationHeight,BufferedImage.TYPE_INT_RGB);
Graphics2D graphics
=
destinationImage.createGraphics();
graphics.drawImage(scaledImage,
0
,
0
,
null
);
}
//
如果是bmp或者gif,则缩略图为png格式
if
(extension.equalsIgnoreCase(
"
bmp
"
)
||
extension.equalsIgnoreCase(
"
gif
"
))
{
extension
=
"
png
"
;
destinationFileName
=
destinationFileName.substring(
0
, destinationFileName.lastIndexOf(
'
.
'
))
+
"
.
"
+
extension;
}
File destinationFile
=
new
File(destinationFileName);
ImageIO.write(destinationImage, extension, destinationFile);
}
}
//
生成验证码的方法
public
BufferedImage createValidateImage(HttpSession session)
{
BufferedImage validateImage
=
new
BufferedImage(
80
,
20
,BufferedImage.TYPE_INT_RGB);
Graphics2D graphics
=
validateImage.createGraphics();
//
从characterStorage中随机抽取四个字符生成验证码
int
length
=
characterStorage.length();
char
[] chars
=
new
char
[
4
];
Random rand
=
new
Random();
for
(
int
i
=
0
; i
<
4
; i
++
)
{
int
index
=
rand.nextInt(length);
chars[i]
=
characterStorage.charAt(index);
}
String str
=
new
String(chars);
//
将字符串保存到Session中,以便于验证
session.setAttribute(
"
validateString
"
, str);
//
画字符串到图片中
graphics.setFont(
new
Font(
"
宋体
"
,Font.BOLD,
18
));
graphics.drawString(str,
2
,
16
);
//
随机画干扰直线
for
(
int
i
=
0
; i
<
5
; i
++
)
{
int
x1
=
rand.nextInt(
80
);
int
y1
=
rand.nextInt(
20
);
int
x2
=
rand.nextInt(
80
);
int
y2
=
rand.nextInt(
20
);
graphics.drawLine(x1, y1, x2, y2);
}
return
validateImage;
}
}
写得比较仓促,没有进行重构,所以比较难看一点。下面是测试用例的代码:
package
com.xkland.util;
import
junit.framework.TestCase;
import
javax.imageio.ImageIO;
import
java.awt.image.BufferedImage;
import
java.io.File;
public
class
ImageUtilTest
extends
TestCase
{
public
void
testCreateMicroImage()
throws
Exception
{
ImageUtil util
=
new
ImageUtil();
util.setSourceDir(
"
E:\\
"
);
util.setDestinationDir(
"
F:\\
"
);
util.setWidth(
"
100
"
);
util.setHeight(
"
100
"
);
//
以仅缩放的形式生成缩略图
util.setMode(
"
ScaleOnly
"
);
//
横图像
util.createMicroImage(
"
001.bmp
"
);
//
竖图像
util.createMicroImage(
"
002.jpg
"
);
//
以先裁减后缩放的形式生成缩略图
util.setDestinationDir(
"
G:\\
"
);
util.setMode(
"
ClipAndScale
"
);
//
横图像
util.createMicroImage(
"
001.bmp
"
);
//
竖图像
util.createMicroImage(
"
002.jpg
"
);
}
public
void
testCreateValidateImage()
throws
Exception
{
ImageUtil util
=
new
ImageUtil();
util.setCharacterStorage(
"
ABCDEFGHIJKLMNOPQRSTUVWXYZ北冥有鱼其名为鲲鲲之大不知其几千里也化而为鸟其名为鹏鹏之背不知其几千里也怒而飞其翼若垂天之云是鸟也海运则将徙于南冥南冥者天池也
"
);
BufferedImage image
=
util.createValidateImage();
ImageIO.write(image,
"
jpg
"
,
new
File(
"
F:\\validateImage.jpg
"
));
}
}
运行该测试用例,可以成功的生成缩略图,并且可以生成验证码,生成的验证码如下图:
把以上代码再修改再完善,就可以创建更漂亮一点的图形了。
为了把上面这个ImageUtil类让SpringSide管理起来,并进行灵活的配置,可以在src\main\resources\spring目录下建立beans.xml文件,并如下配置:
<?
xml version="1.0" encoding="UTF-8"
?>
<!
DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd"
>
<
beans
>
<
bean
id
="imageUtil"
class
="com.xkland.util.ImageUtil"
>
<
property
name
="sourceDir"
>
<
value
>
E:\
</
value
>
</
property
>
<
property
name
="destinationDir"
>
<
value
>
F:\
</
value
>
</
property
>
<
property
name
="width"
>
<
value
>
100
</
value
>
</
property
>
<
property
name
="height"
>
<
value
>
100
</
value
>
</
property
>
<
property
name
="mode"
>
<
value
>
ScaleOnly
</
value
>
</
property
>
<
property
name
="characterStorage"
>
<
value
>
ABCDEFGHIJKLMNOPQRSTUVWXYZ北冥有鱼其名为鲲鲲之大不知其几千里也化而为鸟其名为鹏鹏之背不知其几千里也怒而飞其翼若垂天之云是鸟也海运则将徙于南冥南冥者天池也
</
value
>
</
property
>
</
bean
>
</
beans
>
最后,我们再来看看url重写的问题。俗话说得好:“会者不难,难者不会”,刚开始我为了实现文章开头所说的url重写功能,尝试采用的是配置Servlet映射的方法,但是怎么都不成功,后来才想到使用Filter来实现。有时候开源的东西会直接影响人的思路,比如Struts 1.x采用的就是配置Servlet映射的方法,而到了2.0,也改成Filter了。
在Filter中实现url重写比较简单,无非就是分析字符串和替换字符串,这里我就不列代码了。只有想不到,没有做不到,想办法实现我们设计的功能,这便是技术穿刺的作用。