wangyuheng's Blog

time for change

全局异常处理

本文项目已发布到github,后续学习项目也会添加到此工程下,欢迎fork点赞。
https://github.com/wangyuheng/spring-boot-sample

偷懒代码

偷懒是程序员的美德,但是有些偷懒是为了少写代码,有些则是少思考,直接copy。

不知道你有没有见过这种代码,在最外层catch Exception,避免抛出异常信息。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping("/user")
public class UserApi {

@Autowired
private UserService userService;

@GetMapping("/{id}")
public Object getInfo(@PathVariable("id") int id) {
try {
return userService.getUsernameById(id);
} catch (UserException) {
return "用户id异常";
} catch (Exception e) {
return "服务异常,请稍后再试!";
}
}

}

UserService用于模拟异常抛出

1
2
3
4
5
6
7
8
9
10
11
@Service
public class UserService {

public String getUsernameById(long id) {
if (0 == id % 2) {
throw new IllegalArgumentException("error param!");
} else {
throw new UserException("custom exception!");
}
}
}

全局异常处理

自定义异常

通过自定义异常,区分业务异常,并增加errorCode支持,返回给接口调用方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum ErrorCode {
Error(10000, "服务异常"),
UserIdError(10001, "用户id异常");

private int code;
private String message;

ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}

public int getCode() {
return code;
}

public String getMessage() {
return message;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class CustomException extends RuntimeException {

private int errorCode;

public CustomException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}

public int getErrorCode() {
return errorCode;
}
}
1
2
3
4
5
6
7
8
9
public class UserException extends CustomException {
public UserException(String message) {
super(ErrorCode.UserIdError.getCode(), message);
}

public UserException() {
super(ErrorCode.UserIdError.getCode(), ErrorCode.UserIdError.getMessage());
}
}

@ControllerAdvice

会应用到所有的Controller中的@RequestMapping注解的方法中,通过annotations = RestController.class指定代理的Controller类中。

@ExceptionHandler

用于捕获异常,@ExceptionHandler本身只能捕获当前类的异常信息,结合@ControllerAdvice可以捕获全部controller异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@ControllerAdvice(annotations = RestController.class)
@ResponseBody
public class GlobalExceptionHandler {

private Map<String, Object> getErrorObject(int code, String message) {
Map<String, Object> error = new HashMap<>();
error.put("code", code);
error.put("message", message);
return error;
}

@ExceptionHandler(Exception.class)
public Object exceptionHandler() {
return getErrorObject(ErrorCode.Error.getCode(), ErrorCode.Error.getMessage());
}

@ExceptionHandler(CustomException.class)
public Object customException(CustomException e) {
return getErrorObject(e.getErrorCode(), e.getMessage());
}

}

优化代码

优化后的代码简洁了,无需每次try…catch,统一管理异常信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/user")
public class UserApi {

@Autowired
private UserService userService;

@GetMapping("/{id}")
public Object getInfo(@PathVariable("id") int id) {
return userService.getUsernameById(id);
}

}

测试

mockMvc请求api接口,判断返回值

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
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserApiTest {

private MockMvc mockMvc;

@Autowired
private WebApplicationContext context;

@Autowired
private UserApi userApi;

@Before
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}

private JSONObject getUserApiResult(int id) throws Exception {
String path = "/user/" + id;
return new JSONObject(mockMvc.perform(MockMvcRequestBuilders.get(path))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn()
.getResponse()
.getContentAsString());
}

@Test
public void test_exception_handler() throws Exception {
int i = 1;
assertTrue(ErrorCode.UserIdError.getCode() == getUserApiResult(i).getInt("code"));
i++;
assertTrue(ErrorCode.Error.getCode() == getUserApiResult(i).getInt("code"));
}

}

本文项目已发布到github,后续学习项目也会添加到此工程下,欢迎fork点赞。
https://github.com/wangyuheng/spring-boot-sample

你的java应用在运行时对你来说是黑盒吗?你可以查看到springboot运行时的各种信息吗?

Spring Boot Actuator

springboot提供了用于健康检测的endpoint,提供了查看系统信息、内存、环境等。

1. 添加依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2. 访问链接

应用启动后访问 http://localhost:8080/health 查看应用启动状态。
端口号可以通过配置文件management.port=9527变更

常用endpoint

  1. http://localhost:8080/env 环境变量
  2. http://localhost:8080/info 应用信息
  3. http://localhost:8080/metrics 内存等应用基本指标
  4. http://localhost:8080/dump 线程栈
  5. http://localhost:8080/configprops 配置项

3. 开启全部endpoint & 关闭验权

部分请求会返回401, 这是因为endpoint未开启,或者开启了登录验权,可以通过配置文件进行配置

1
2
management.security.enabled=false
endpoints.enabled=true

自定义endpoint

一、实现Endpoint接口

1
2
3
4
5
6
7
8
9
public interface Endpoint<T> {
String getId();

boolean isEnabled();

boolean isSensitive();

T invoke();
}
  1. getId(), 指定了endpoint访问url
  2. isEnabled(), 表示是否启用
  3. isSensitive(), 表示是否验权
  4. invoke(), 页面返回值

实现类通过@Bean的形式注入后,再次启动应用,即可通过url访问,并返回invoke返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CustomEndpoint implements Endpoint {

@Override
public String getId() {
return "custom";
}

@Override
public boolean isEnabled() {
return true;
}

@Override
public boolean isSensitive() {
return false;
}

@Override
public Object invoke() {
return "hello endpoint";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
public class EndpointApplication {

public static void main(String[] args) {
SpringApplication.run(EndpointApplication.class, args);
}


@Bean
public CustomEndpoint customEndpoint() {
return new CustomEndpoint();
}
}

访问http://localhost:8080/custom 可以看到invoke返回的内容。

但是这样,每个endpoint都需要单独注入,且没有层级、通配符,不方便管理,为满足需求,尝试做了如下改造

二、继承EndpointMvcAdapter

2.1 自定义 EndpointAction 接口

用于定制endpoint处理行为,可以理解为invoke方法的具体实施者,名称用来管理访问路径

1
2
3
4
5
public interface EndpointAction extends Serializable {
Object execute();

String getName();
}
2.2 EndpointAction的多种实现

需要实现的功能,如:读取配置文件、查看内存信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class PropertiesAction implements EndpointAction {

@Override
public Object execute() {
try {
return PropertiesLoaderUtils.loadAllProperties("application.properties");
} catch (IOException e) {
return "read application fail! error: " + e.getMessage();
}
}

@Override
public String getName() {
return "properties";
}
}
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
@Component
public class VMAction implements EndpointAction {

private static final VMAction INSTANCE = new VMAction();

private String version;
private String startTime;
private String initHeap;
private String maxHeap;
private Set<String> arguments;

@Override
public Object execute() {
INSTANCE.version = System.getProperty("java.runtime.version");
INSTANCE.startTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(ManagementFactory.getRuntimeMXBean().getStartTime());
INSTANCE.initHeap = String.valueOf(ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getInit() / 1024 / 1024).concat("MB");
INSTANCE.maxHeap = String.valueOf(ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax() / 1024 / 1024).concat("MB");
INSTANCE.arguments = new HashSet<>(ManagementFactory.getRuntimeMXBean().getInputArguments());
return INSTANCE;
}

@Override
public String getName() {
return "vm";
}

public String getVersion() {
return version;
}

public String getStartTime() {
return startTime;
}

public String getInitHeap() {
return initHeap;
}

public String getMaxHeap() {
return maxHeap;
}

public Set<String> getArguments() {
return arguments;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class DefaultAction implements EndpointAction {


private static final DefaultAction INSTANCE = new DefaultAction();

public static DefaultAction getInstance() {
return INSTANCE;
}

@Override
public Object execute() {
return "try /help for action list";
}

@Override
public String getName() {
return "default";
}

}
2.3 继承EndpointMvcAdapter
  1. 注入action map,根据name获取bean实现
  2. 通过url mapping匹配action
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
public class CustomEndpointAdapter extends EndpointMvcAdapter {


private Map<String, EndpointAction> endpointActionMap = new HashMap<>();

@Autowired
public void setEndpointActionMap(List<EndpointAction> endpointActionList) {
endpointActionList.forEach(endpointAction -> endpointActionMap.put(endpointAction.getName(), endpointAction));
}

public CustomEndpointAdapter() {
super(new CustomEndpoint());
}

@RequestMapping(value = "/{name:.*}",
method = RequestMethod.GET, produces = {
ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON_VALUE,
MediaType.APPLICATION_JSON_VALUE}
)
@ResponseBody
@HypermediaDisabled
public Object dispatcher(@PathVariable String name) {
if ("help".equalsIgnoreCase(name)) {
return endpointActionMap.keySet().stream().map(key -> getName() + "/" + key).collect(toSet());
} else {
return endpointActionMap.getOrDefault(name, DefaultAction.getInstance()).execute();
}
}

}
2.4 定制endpoint作为入口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CustomEndpoint implements Endpoint {

@Override
public String getId() {
return "custom";
}

@Override
public boolean isEnabled() {
return true;
}

@Override
public boolean isSensitive() {
return false;
}

@Override
public Object invoke() {
return DefaultAction.getInstance().execute();
}
}
2.5 启动时注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootApplication
public class EndpointApplication {

public static void main(String[] args) {
SpringApplication.run(EndpointApplication.class, args);
}


@Bean
@ConditionalOnClass(CustomEndpoint.class)
public CustomEndpointAdapter customEndpointAdapter() {
return new CustomEndpointAdapter();
}
}

测试

揪心的测试环节,启动webEnvironment环境,通过TestRestTemplate访问endpoint的mapping,比对返回值即可

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
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PropertiesActionTest {

private TestRestTemplate restTemplate;

@Value("${management.port}")
private String managementPort;

@Before
public void setupMockMvc() {
restTemplate = new TestRestTemplate();
}

@Test
public void test_properties_action() throws Exception {
String path = "http://localhost:" + managementPort + "/custom/properties";
Map<String, String> result = restTemplate.getForObject(path, HashMap.class);
assertEquals(result.get("management.port"), managementPort);
}

@Test
public void test_help() throws Exception {
String path = "http://localhost:" + managementPort + "/custom/help";
Set<String> result = restTemplate.getForObject(path, Set.class);
assertTrue(result.contains("custom/properties"));
}

@Test
public void test_rand() throws Exception {
String path = "http://localhost:" + managementPort + "/custom/" + new Random().nextInt();
String result = restTemplate.getForObject(path, String.class);
assertEquals("try /help for action list", result);

}

}

本文项目已发布到github,后续学习项目也会添加到此工程下,欢迎fork点赞。
https://github.com/wangyuheng/spring-boot-sample

需要具备少量aop基础,通过springboot构建项目方便演示。

AOP-面向切面编程

一句话描述,在java对象增加切点,在不改变对象的前提下通过代理扩展功能。

http日志打印拦截器

restful api

通过springboot快速搭建一个RestController接口。

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
import org.springframework.web.bind.annotation.*;
import wang.crick.study.httplog.annotation.HttpLog;
import wang.crick.study.httplog.domain.User;

import java.util.Random;

@RestController
@RequestMapping("/user")
public class UserApi {

@GetMapping("/log/{id}")
public RestApiResponse<User> getInfo(@PathVariable("id") int id,
@RequestParam("age") int age){
User user = new User();
user.setId(id);
user.setUsername(String.valueOf(new Random().nextLong()));
user.setAge(age);
return RestApiResponse.success(user);
}

@GetMapping("/log/pwd/{id}")
public RestApiResponse<User> getInfoWithPwd(@PathVariable("id") int id,
@RequestHeader("username") String username,
@RequestHeader("password") String password){
User user = new User();
user.setId(id);
user.setUsername(username);
user.setPassword(password);
return RestApiResponse.success(user);
}

@GetMapping("/log/pwdExcludeResponse/{id}")
public RestApiResponse<User> getInfoWithPwd(@PathVariable("id") int id,
@RequestParam("age") int age,
@RequestHeader("password") String password){
User user = new User();
user.setId(id);
user.setPassword(password);
user.setAge(age);
return RestApiResponse.success(user);
}

}

切面选择

一般教程会选择拦截所有http请求,并打印request.parameters。但是存在问题:

  1. 不够灵活,部分参数不想打印,如文件数据(过大)、敏感数据(身份证)等。
  2. 显式的标注日志输出,避免给维护人员造成疑惑。
  3. 部分参数通过header传输

因此,自定义日志输出@annotation HttpLog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HttpLog {
/**
* 忽略参数,避免文件or无意义参数打印
*
* @return 忽略参数数组
*/
String[] exclude() default {};

/**
* 需要打印的header参数
*
* @return header参数名数组
*/
String[] headerParams() default {};

boolean ignoreResponse() default false;
}

获取HttpServletRequest

spring通过ThreadLocal持有request参数。

1
2
3
4
5
private HttpServletRequest getRequest() {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
return sra.getRequest();
}

获取uri

根据拦截规则不同,getServletPath()和request.getPathInfo()可能为空,简单的做一次健壮性判断。

1
2
3
4
5
private String getRequestPath(HttpServletRequest request) {
return (null != request.getServletPath() && request.getServletPath().length() > 0)
? request.getServletPath() : request.getPathInfo();

}

aop日志输出

  1. Pointcut自定义@annotation HttpLog
  2. 拿到@annotation,读取自定义属性,如忽略response、打印headers等
  3. 遍历request & headers中需打印参数
  4. 定制日志格式并打印log
  5. 拦截返回值并打印log

Aspect代码如下

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
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import wang.crick.study.httplog.annotation.HttpLog;
import wang.crick.study.httplog.api.RestApiResponse;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

@Aspect
public class HttpLogAspect {

private Logger log = LoggerFactory.getLogger(HttpLogAspect.class);

@Pointcut("@annotation(wang.crick.study.httplog.annotation.HttpLog)")
public void logAnnotation() {
}

private Optional<HttpLog> getLogAnnotation(JoinPoint joinPoint) {
if (joinPoint instanceof MethodInvocationProceedingJoinPoint) {
Signature signature = joinPoint.getSignature();
if (signature instanceof MethodSignature) {
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method.isAnnotationPresent(HttpLog.class)) {
return Optional.of(method.getAnnotation(HttpLog.class));
}
}
}
return Optional.empty();
}

private HttpServletRequest getRequest() {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
return sra.getRequest();
}

private String getRequestPath(HttpServletRequest request) {
return (null != request.getServletPath() && request.getServletPath().length() > 0)
? request.getServletPath() : request.getPathInfo();

}

@Before("logAnnotation()")
public void requestLog(JoinPoint joinPoint) {
try {
Optional<HttpLog> httpLog = getLogAnnotation(joinPoint);
httpLog.ifPresent(anno -> {
HttpServletRequest request = getRequest();
List<String> excludes = Arrays.asList(anno.exclude());
List<Object> params = new ArrayList<>();
StringBuilder logMsg = new StringBuilder();
logMsg.append("REQUEST_LOG. sessionId:{}. ")
.append("requestUrl: ")
.append(getRequestPath(request))
.append(" -PARAMS- ");
params.add(request.getSession().getId());
request.getParameterMap().forEach((k, v) -> {
if (!excludes.contains(k)) {
logMsg.append(k).append(": {}, ");
params.add(v);
}
});
if (anno.headerParams().length > 0) {
logMsg.append(" -HEADER_PARAMS- ");
Arrays.asList(anno.headerParams()).forEach(param -> {
logMsg.append(param).append(": {}, ");
params.add(request.getHeader(param));
});
}
log.info(logMsg.toString(), params.toArray());
});
} catch (Exception ignore) {
log.warn("print request log fail!", ignore);
}
}


@AfterReturning(returning = "restApiResponse", pointcut = "logAnnotation()")
public void response(JoinPoint joinPoint, RestApiResponse restApiResponse) {
try {
Optional<HttpLog> httpLog = getLogAnnotation(joinPoint);
httpLog.ifPresent(anno -> {
if (!anno.ignoreResponse()) {
log.info("RESPONSE_LOG. sessionId:{}. result:{}", getRequest().getSession().getId(), restApiResponse);
}
});
} catch (Exception ignore) {
log.warn("print response log fail!", ignore);
}
}

}

使用

在RestController中增加自定义注解HttpLog

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
import org.springframework.web.bind.annotation.*;
import wang.crick.study.httplog.annotation.HttpLog;
import wang.crick.study.httplog.domain.User;

import java.util.Random;

@RestController
@RequestMapping("/user")
public class UserApi {


/**
* curl -H 'username:12b4' -H 'password:34ndd' -v 'http://localhost:8080/user/log/123?age=32'
*/
@GetMapping("/log/{id}")
@HttpLog()
public RestApiResponse<User> getInfo(@PathVariable("id") int id,
@RequestParam("age") int age){
User user = new User();
user.setId(id);
user.setUsername(String.valueOf(new Random().nextLong()));
user.setAge(age);
return RestApiResponse.success(user);
}

/**
* curl -H 'username:12b4' -H 'password:34ndd' -v 'http://localhost:8080/user/log/pwd/123?age=32'
*/
@GetMapping("/log/pwd/{id}")
@HttpLog(headerParams="password")
public RestApiResponse<User> getInfoWithPwd(@PathVariable("id") int id,
@RequestHeader("username") String username,
@RequestHeader("password") String password){
User user = new User();
user.setId(id);
user.setUsername(username);
user.setPassword(password);
return RestApiResponse.success(user);
}

/**
* curl -H 'username:12b4' -H 'password:34ndd' -v 'http://localhost:8080/user/log/pwdExcludeResponse/123?age=32'
*/
@GetMapping("/log/pwdExcludeResponse/{id}")
@HttpLog(headerParams="username", ignoreResponse = true)
public RestApiResponse<User> getInfoWithPwd(@PathVariable("id") int id,
@RequestParam("age") int age,
@RequestHeader("password") String password){
User user = new User();
user.setId(id);
user.setPassword(password);
user.setAge(age);
return RestApiResponse.success(user);
}

}

加载HttpLogAspect对象

可以在@SpringBootApplication类下直接加在,也可以在HttpLogAspect中增加@Component注解,推荐前者,更清晰。不需要增加@EnableAspectJAutoProxy类 (注:1)

1
2
3
4
@Bean
public HttpLogAspect httpLogAspect(){
return new HttpLogAspect();
}

启动容器并访问

header参数可以通过curl验证。**(注:2)**

测试

可测试的代码才是好代码。所以其实我没写过几行好代码。。。

日志输出到控制台,可以通过获取控制台中内容进行contains验证

获取控制台输出字符

1
2
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));

执行request请求

基于spring test提供的mockMvc方法,测试代码如下:

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
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.Random;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserApiTest {

private MockMvc mockMvc;

@Autowired
private UserApi userApi;

@Autowired
private WebApplicationContext context;

private ByteArrayOutputStream outContent;
private int userId = new Random().nextInt(10);
private int age = new Random().nextInt(10);
private long username = new Random().nextLong();
private long password = new Random().nextLong();

@Before
public void setup() {
// 坚挺控制台输出
outContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));

//项目拦截器有效
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
//单个类,拦截器无效
// mockMvc = MockMvcBuilders.standaloneSteup(userApi).build();
}


@Test
public void test_log() throws Exception {
String path = "/user/log/" + userId;
String uri = path + "?age=" + age;
RequestBuilder request = MockMvcRequestBuilders.get(uri)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON);

mockMvc.perform(request).andExpect(MockMvcResultMatchers.status().isOk());
String console = outContent.toString();
assertTrue(console.contains("REQUEST_LOG"));
assertFalse(console.contains("HEADER_PARAMS"));
assertTrue(console.contains("RESPONSE_LOG"));
assertTrue(console.contains(path));
assertTrue(console.contains(String.valueOf(age)));
assertFalse(console.contains(String.valueOf(username)));
assertFalse(console.contains(String.valueOf(password)));
}

@Test
public void test_log_header() throws Exception {
String path = "/user/log/pwd/" + userId;
String uri = path + "?age=" + age;
RequestBuilder request = MockMvcRequestBuilders.get(uri)
.header("username", username)
.header("password", password)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON);

mockMvc.perform(request).andExpect(MockMvcResultMatchers.status().isOk());

String console = outContent.toString();
assertTrue(console.contains("REQUEST_LOG"));
assertTrue(console.contains("HEADER_PARAMS"));
assertTrue(console.contains("RESPONSE_LOG"));
assertTrue(console.contains(path));
assertTrue(console.contains(String.valueOf(age)));
assertFalse(console.contains(String.valueOf(username)));
assertTrue(console.contains(String.valueOf(password)));
}

@Test
public void test_log_header_excludeResponse() throws Exception {
String path = "/user/log/pwdExcludeResponse/" + userId;
String uri = path + "?age=" + age;
RequestBuilder request = MockMvcRequestBuilders.get(uri)
.header("username", username)
.header("password", password)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON);

mockMvc.perform(request).andExpect(MockMvcResultMatchers.status().isOk());

String console = outContent.toString();
assertTrue(console.contains("REQUEST_LOG"));
assertTrue(console.contains("HEADER_PARAMS"));
assertFalse(console.contains("RESPONSE_LOG"));
assertTrue(console.contains(path));
assertTrue(console.contains(String.valueOf(age)));
assertTrue(console.contains(String.valueOf(username)));
assertFalse(console.contains(String.valueOf(password)));
}

}

注:1 @EnableAspectJAutoProxy

注:2 curl增加header参数

1
curl -H 'username:123' -H 'password:345'

编写单元测试过程中,接口入参为照片文件byte[],想到的方法是在test/resources目录下,放好测试照片。代码编写遇到两个问题

1. 读取resources目录下文件

1
ClassPathResource resource = new ClassPathResource("front.png");

2. Resource转换为byte[]

Resource有getFile()接口,InputStreamSource有getInputStream()接口,ClassPathResource作为子类,可以通过这两个接口方法,然后再进行转换。还有一个更简便的方法,通过Files工具读取path
注:Files读取的path不是ClassPathResource#getPath()的返回值,而是ClassPathResource#getURL()#getPath()的值

1
byte[] bytes = Files.readAllBytes(Paths.get(resource.getURL().getPath()));

spring-boot-swagger-starter

本文项目已发布到github,欢迎fork点赞。
https://github.com/wangyuheng/spring-boot-swagger-starter
已发布maven中央仓库,欢迎使用。

1
2
3
4
5
<dependency>
<groupId>com.github.wangyuheng</groupId>
<artifactId>swagger-starter</artifactId>
<version>0.0.1</version>
</dependency>

swagger

swagger可以快速生成restful接口文档,并提供在线调试功能。通过springboot开发微服务时,swagger文档可以极大的提高协作效率。

springfox

springfox 将swagger整合到spring

定制starter

1. 定义swagger属性

用于文档页面显示及接口扫描路径,并使用@ConfigurationProperties读取属性

1
2
3
4
5
6
7
8
9
10
11
12
swagger:
groupName: 分类(groupName)
title: 标题(title)
description: 介绍(description)
termsOfServiceUrl: 服务URL(termsOfServiceUrl)
version: 版本(version)
contactName: 作者名(contactName)
contactUrl: 作者主页(contactUrl)
contactEmail: 作者邮箱(contactEmail)
paths: /upload.*,/category.*
license:
licenseUrl:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "swagger")
public class SwaggerProperty {

private String groupName;
private String title;
private String description;
private String termsOfServiceUrl;
private String version;
private String license;
private String licenseUrl;
//Contact
private String contactName;
private String contactUrl;
private String contactEmail;
private String[] paths;
}

2.设置swagger自动配置类

重点在于**@EnableSwagger2及声明Docket**

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
import com.google.common.base.Predicate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import pro.hemo.swagger.config.SwaggerProperty;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import static com.google.common.base.Predicates.or;
import static springfox.documentation.builders.PathSelectors.regex;

@EnableSwagger2
@EnableConfigurationProperties(SwaggerProperty.class)
public class SwaggerAutoApplication {

@Autowired
private SwaggerProperty swaggerProperty;

@Bean
public Docket docket() {
checkValid(swaggerProperty);
Predicate<String>[] selector = new Predicate[]{};
if (null != swaggerProperty.getPaths() && swaggerProperty.getPaths().length > 0) {
selector = new Predicate[swaggerProperty.getPaths().length];
for (int i = 0; i < selector.length; i++) {
selector[i] = regex(swaggerProperty.getPaths()[i]);
}
}

return new Docket(DocumentationType.SWAGGER_2)
.groupName(swaggerProperty.getGroupName())
.apiInfo(apiInfo())
.select()
.paths(or(selector))
.build();
}

private void checkValid(SwaggerProperty swaggerProperty) {
}


private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(swaggerProperty.getTitle())
.description(swaggerProperty.getDescription())
.termsOfServiceUrl(swaggerProperty.getTermsOfServiceUrl())
.contact(new Contact(swaggerProperty.getContactName(), swaggerProperty.getContactUrl(), swaggerProperty.getContactEmail()))
.license(swaggerProperty.getLicense())
.licenseUrl(swaggerProperty.getLicenseUrl())
.version(swaggerProperty.getVersion())
.build();
}

}

3. 创建spring.factories

用于指定springboot自动配置文件路径,目录为src/main/resources/META-INF/spring.factories

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
pro.hemo.swagger.SwaggerAutoApplication

使用starter

1. 添加依赖

通过maven添加starter

1
2
3
4
5
<dependency>
<groupId>pro.hemo</groupId>
<artifactId>swagger-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

因为没有发布到仓库,需要先将starter项目发布到本地仓库
SwaggerStarter目录执行

1
mvn clean install 

2. 编写Restful接口

Spring可以很方便的编写Restful接口,可以添加**@Api**等注解,用于生成文档。注解在io.swagger.annotations包下,后续会介绍常用注解。

3. 添加配置文件

通过path指定接口扫描目录

1
2
3
4
5
6
7
8
9
10
11
12
swagger:
groupName:
title:
description:
termsOfServiceUrl:
version:
contactName:
contactUrl:
contactEmail:
paths: /upload.*, /category.*
license:
licenseUrl:

4. 启动项目

启动springboot项目,访问http://localhost:8080/swagger-ui.html 可以看到接口列表。
springboot-swagger-00.png

这里有几点需要注意

  1. swagger-ui.html 为自动生成映射
  2. 注意静态资源不要被拦截
  3. swagger-ui.html为页面,接口以json格式通过加载并渲染。json地址为http://localhost:8080/v2/api-docs?group

控制台输出调试

为了方便测试,在启动springboot项目后,期望通过控制台输出,查看运行。
此时可以通过让**@SpringBootApplication类implements CommandLineRunner或者ApplicationRunner**接口,并实现run方法。

Read more »

spring-boot Kafka

基于springboot和kafka开发消息通信系统,提供消息发送、接收功能。
本文需要对kafka有基础的了解,且存在可访问的kafka服务。如果缺乏相关知识,请先阅读kafka官方文档

Read more »

在上传图片的时候,希望在浏览器上能预览图片。

之前的做法是ajax方式上传图片,返回服务器的图片访问地址,本地预览。

在想有没有办法,不上传直接预览。之前记得是因为安全性原因,不能预览。但是着找到了这么个示例,贴出来大家看看。兼容性没去校验。

Read more »

github地址: https://github.com/wangyuheng/painter

DEMO地址: http://painter.crick.wang/

变形

变形这部分代码比较多,所以分为上下两部分。在(上)中已经实现了左侧中间点的变形操作。但是一共有8个操作点,为了避免混乱,抽离handle.border.listener.js提供8个点的变形方法。

y轴移动

之前左侧中间点的操作可以视为x轴移动,现在只需要实现y轴移动的算法,即可实现8个点的移动。因为角落的四个点其实就是同时向2个方向移动。

以topCenter为例,设定Top,以上为正方向,1为向上运动,-1为向下运动。

var tTop = 1;

计算鼠标y轴移动距离

var dy = currPoint.y - lastPoint.y;

屏幕左上角为(0,0)点,所以图形的新高度为

var height = ele.height() - tTop * dy;

判断height,如果小于0,说明达到图形的y轴0点,要根据y轴进行翻转。

var height = ele.height() - tTop * dy;
if (height > 0) {
    var newY = ele.y() + tTop * dy;
    ele.y(newY).height(height);
} else {
    //invert
    tTop = -tTop;
    ele.height(-height).matrix(new SVG.Matrix(ele).flip('y', ele.bbox().cy));
}

成员变量

根据x、y两个方向的移动,推测出需要如下成员变量

var lastPoint;
var xLeft;
var yTop;
var ele;

ele可以在初始化时完成,为当前操作的元素,lastPoint、xLeft、yTop需要在开始拖拽时初始化。

通用方法

为了节省代码,抽象了4个通用方法left(dx)、right(dx)、top(dy)、bottom(dy)

8个操作点根据需要,获取当前的鼠标移动距离,并调用其中的1或2个通用方法,即可实现变形效果。

绑定并触发

在构造函数中,除了初始化ele,还需要给ele绑定对应的事件。

在HandleBorder中,监听8个操作点的dragstart和dragmove事件,调用对应方法。

var HandleBorderListener = function(targetElement) {
    ele = targetElement;
    ele.on('leftCenterDragStart', leftCenterDragStart);
    ele.on('leftCenterDragMove', leftCenterDragMove);
    ele.on('rightCenterDragStart', rightCenterDragStart);
    ele.on('rightCenterDragMove', rightCenterDragMove);
    ele.on('topCenterDragStart', topCenterDragStart);
    ele.on('topCenterDragMove', topCenterDragMove);
    ele.on('bottomCenterDragStart', bottomCenterDragStart);
    ele.on('bottomCenterDragMove', bottomCenterDragMove);
    ele.on('leftTopDragStart', leftTopDragStart);
    ele.on('leftTopDragMove', leftTopDragMove);
    ele.on('leftBottomDragStart', leftBottomDragStart);
    ele.on('leftBottomDragMove', leftBottomDragMove);
    ele.on('rightTopDragStart', rightTopDragStart);
    ele.on('rightTopDragMove', rightTopDragMove);
    ele.on('rightBottomDragStart', rightBottomDragStart);
    ele.on('rightBottomDragMove', rightBottomDragMove);
}

在HandleBorder中,在create方法中

_this.rectLeftCenter.on("dragstart", function() {
    _this.currentElement.fire("leftCenterDragStart", {
        currPoint: event.detail.p
    });
});
_this.rectLeftCenter.on("dragmove", function() {
    _this.currentElement.fire("leftCenterDragMove", {
        currPoint: event.detail.p
    });
});
_this.rectLeftCenter.on("afterdragmove", function() {
    _this.rebound(_this.currentElement.bbox());
});

完整代码

完整代码如下

(function() {
    var lastPoint;
    var xLeft;
    var yTop;
    var ele;

    function left(dx) {
        var width = ele.width() - xLeft * dx;
        if (width > 0) {
            var newX = ele.x() + xLeft * dx;
            ele.x(newX).width(width);
        } else {
            //invert
            xLeft = -xLeft;
            ele.x(ele.bbox().x2).width(-width).matrix(new SVG.Matrix(ele).flip('x', ele.bbox().cx));
        }
    }

    function right(dx) {
        var width = ele.width() - xLeft * dx;
        if (width > 0) {
            ele.width(width);
        } else {
            //invert
            xLeft = -xLeft;
            ele.width(-width).matrix(new SVG.Matrix(ele).flip('x', ele.bbox().cx));
        }
    }

    function top(dy) {
        var height = ele.height() - tTop * dy;
        if (height > 0) {
            var newY = ele.y() + tTop * dy;
            ele.y(newY).height(height);
        } else {
            //invert
            tTop = -tTop;
            ele.height(-height).matrix(new SVG.Matrix(ele).flip('y', ele.bbox().cy));
        }
    }

    function bottom(dy) {
        var height = ele.height() - tTop * dy;
        if (height > 0) {
            ele.height(height);
        } else {
            //invert
            tTop = -tTop;
            ele.height(-height).matrix(new SVG.Matrix(ele).flip('y', ele.bbox().cy));
        }
    }

    function leftCenterDragStart(data) {
        lastPoint = data.detail.currPoint;
        xLeft = 1;
    }

    function leftCenterDragMove(data) {
        var currPoint = data.detail.currPoint;
        var dx = currPoint.x - lastPoint.x;
        left(dx);
        lastPoint = currPoint;
    }

    function rightCenterDragStart(data) {
        lastPoint = data.detail.currPoint;
        xLeft = -1;
    }

    function rightCenterDragMove(data) {
        var currPoint = data.detail.currPoint;
        var dx = currPoint.x - lastPoint.x;
        right(dx);
        lastPoint = currPoint;
    }

    function topCenterDragStart(data) {
        lastPoint = data.detail.currPoint;
        tTop = 1;
    }

    function topCenterDragMove(data) {
        var currPoint = data.detail.currPoint;
        var dy = currPoint.y - lastPoint.y;
        top(dy);
        lastPoint = currPoint;
    }

    function bottomCenterDragStart(data) {
        lastPoint = data.detail.currPoint;
        tTop = -1;
    }

    function bottomCenterDragMove(data) {
        var currPoint = data.detail.currPoint;
        var dy = currPoint.y - lastPoint.y;
        bottom(dy);
        lastPoint = currPoint;
    }

    function leftTopDragStart(data) {
        lastPoint = data.detail.currPoint;
        xLeft = 1;
        tTop = 1;
    }

    function leftTopDragMove(data) {
        var currPoint = data.detail.currPoint;
        var dx = currPoint.x - lastPoint.x;
        var dy = currPoint.y - lastPoint.y;
        left(dx);
        top(dy);
        lastPoint = currPoint;
    }

    function leftBottomDragStart(data) {
        lastPoint = data.detail.currPoint;
        xLeft = 1;
        tTop = -1;
    }

    function leftBottomDragMove(data) {
        var currPoint = data.detail.currPoint;
        var dx = currPoint.x - lastPoint.x;
        var dy = currPoint.y - lastPoint.y;
        left(dx);
        bottom(dy);
        lastPoint = currPoint;
    }

    function rightTopDragStart(data) {
        lastPoint = data.detail.currPoint;
        xLeft = -1;
        tTop = 1;
    }

    function rightTopDragMove(data) {
        var currPoint = data.detail.currPoint;
        var dx = currPoint.x - lastPoint.x;
        var dy = currPoint.y - lastPoint.y;
        right(dx);
        top(dy);
        lastPoint = currPoint;
    }

    function rightBottomDragStart(data) {
        lastPoint = data.detail.currPoint;
        xLeft = -1;
        tTop = -1;
    }

    function rightBottomDragMove(data) {
        var currPoint = data.detail.currPoint;
        var dx = currPoint.x - lastPoint.x;
        var dy = currPoint.y - lastPoint.y;
        right(dx);
        bottom(dy);
        lastPoint = currPoint;
    }

    var HandleBorderListener = function(targetElement) {
        ele = targetElement;
        ele.on('leftCenterDragStart', leftCenterDragStart);
        ele.on('leftCenterDragMove', leftCenterDragMove);
        ele.on('rightCenterDragStart', rightCenterDragStart);
        ele.on('rightCenterDragMove', rightCenterDragMove);
        ele.on('topCenterDragStart', topCenterDragStart);
        ele.on('topCenterDragMove', topCenterDragMove);
        ele.on('bottomCenterDragStart', bottomCenterDragStart);
        ele.on('bottomCenterDragMove', bottomCenterDragMove);
        ele.on('leftTopDragStart', leftTopDragStart);
        ele.on('leftTopDragMove', leftTopDragMove);
        ele.on('leftBottomDragStart', leftBottomDragStart);
        ele.on('leftBottomDragMove', leftBottomDragMove);
        ele.on('rightTopDragStart', rightTopDragStart);
        ele.on('rightTopDragMove', rightTopDragMove);
        ele.on('rightBottomDragStart', rightBottomDragStart);
        ele.on('rightBottomDragMove', rightBottomDragMove);
    }

    this.HandleBorderListener = HandleBorderListener;
})();

github地址: https://github.com/wangyuheng/painter

DEMO地址: http://painter.crick.wang/

变形

既然实现了拖拽效果,就可以在此基础上,实现另一个效果:变形。

HandlerBorder

在实现变形效果之前,先讲解一下HandlerBorder。这是在pick时,选中元素后,在元素周围出现的4个黑框。 现在将其扩展为8个黑框,拖拽黑框,实现元素的变形效果。

先分析一下HandlerBorder的代码

(function() {

    var sideLength = 8;
    var sideWidth = {
        width: 1
    };

    var HandleBorder = function(svgDoc) {
        this.init(svgDoc);
    }

    HandleBorder.prototype = {
        constructor: HandleBorder,
        init: function(svgDoc) {
            this.currentSvgDoc = svgDoc;
            this.create();
            return this;
        },
    };

    HandleBorder.prototype.create = function() {
        var _this = this;

        _this.handleBorderGroup = _this.currentSvgDoc.group();

        _this.blockGroup = _this.handleBorderGroup.group();

        _this.rectLeftTop = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth);
        _this.rectLeftBottom = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth);
        _this.rectRightTop = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth);
        _this.rectRightBottom = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth);

        _this.rectLeftCenter = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth);
        _this.rectRightCenter = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth);
        _this.rectTopCenter = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth);
        _this.rectBottomCenter = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth);

    };

    HandleBorder.prototype.rebound = function(bbox) {
        var _this = this;

        var x1 = bbox.x;
        var y1 = bbox.y;
        var x2 = bbox.x2;
        var y2 = bbox.y2;
        _this.rectLeftTop.move(x1 - sideLength, y1 - sideLength);
        _this.rectLeftBottom.move(x1 - sideLength, y2);
        _this.rectRightTop.move(x2, y1 - sideLength);
        _this.rectRightBottom.move(x2, y2);

        _this.rectLeftCenter.move(x1 - sideLength, (y2 + y1 - sideLength) / 2);
        _this.rectRightCenter.move(x2, (y2 + y1 - sideLength) / 2);
        _this.rectTopCenter.move((x2 + x1 - sideLength) / 2, y1 - sideLength);
        _this.rectBottomCenter.move((x2 + x1 - sideLength) / 2, y2);

    };

    HandleBorder.prototype.show = function(svgEle) {
        if (!svgEle) {
            return;
        }
        this.currentElement = svgEle;
        this.handleBorderGroup.show();

        this.rebound(svgEle.bbox());
    };

    HandleBorder.prototype.hide = function() {
        this.handleBorderGroup.hide();
    };

    this.HandleBorder = HandleBorder;

})();
  1. HandleBorder有一个构造函数,调用create方法。create方法中创建了一个group,并在group中创建8个矩形。
  2. 在调用HandleBorder时,先new一个实例,然后调用其show方法,先将8个矩形的group显示出来,再重新设定其位置rebound。
  3. rebound方法根据元素边框坐标和小矩形的边长,计算其位置,分布在4个边角与4个边长中点。

变形原理

  1. 将HandleBorder中的8个小矩形,作为操作框,绑定可拖拽效果。
  2. 拖拽时,判断其移动距离与方向,使图形对应改变宽高,达到变形效果。
  3. 为了避免拖拽时,操作框移动到任意位置,每次拖拽时,重新设置其位置rebound。

代码实现

实现拖拽

小矩形增加.draggable()方法调用。

_this.rectLeftTop = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();
_this.rectLeftBottom = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();
_this.rectRightTop = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();
_this.rectRightBottom = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();

_this.rectLeftCenter = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();
_this.rectRightCenter = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();
_this.rectTopCenter = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();
_this.rectBottomCenter = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();
绑定拖拽事件

以左侧中心点操作框 rectLeftCenter 为例

在dragstart时,获取鼠标所在坐标lastPoint

var lastPoint = null;
_this.rectLeftCenter.on("dragstart", function(event) {
    lastPoint = event.detail.p;
});

event.detail中包含4个成员变量,可以参考svg.draggable.js中的代码

this.el.fire('dragstart', {event: e, p: this.startPoints.point, m: this.m, handler: this})

绑定dragmove事件,并重新计算鼠标坐标。

_this.rectLeftCenter.on("dragmove", function() {
    var currPoint = event.detail.p;
    lastPoint = currPoint;
});
计算图形改变

dragmove时可以活动当前坐标和上次坐标,计算出x轴移动的距离

var dx = currPoint.x - lastPoint.x;

定义一个变量xLeft = 1;表明鼠标移动方向,向左移动,则为1, 否则为-1; 并在拖拽开始时,初始化

_this.rectLeftCenter.on("dragstart", function(event) {
    lastPoint = event.detail.p;
    xLeft = 1;
});
    

新的元素宽度为

var width = ele.width() - xLeft * dx;

判断width是否大于0,如果大于0,则表示正向移动, 则新坐标x为

var newX = ele.x() + xLeft * dx;

重新设定元素的x坐标和width,即可实现变形效果。

ele.x(newX).width(width);

如果为反向移动,则width小于 0,此时反转xLeft,坐标原点应为元素的bbox的x2点,并根据x的中点做flip翻转。

xLeft = -xLeft;
ele.x(ele.bbox().x2).width(-width).matrix(new SVG.Matrix(ele).flip('x', ele.bbox().cx));
反转坐标复位

翻转之后的rebound无法复原,需要在rebound同样执行矩阵。

this.blockGroup.matrix(new SVG.Matrix(_this.currentElement));
限制拖拽轨迹

为了避免拖拽时,小矩形可以任意移动,则在拖拽时,执行rebound操作校准。svg.draggable.js默认为提供此事件,扩展svg.draggable.js 增加如下方法。在DragHander.prototype.drag的结尾处增加

// so we can use it in the end-method, too
this.el.fire('afterdragmove', { event: e, p: p, m: this.m, handler: this });

并在小矩形中监听次事件,执行复位校准。

_this.rectLeftCenter.on("afterdragmove", function() {
    _this.rebound(_this.currentElement.bbox());
});

代码

handle.border.js完整代码如下

(function() {

    var sideLength = 8;
    var sideWidth = {
        width: 1
    };

    var HandleBorder = function(svgDoc) {
        this.init(svgDoc);
    }

    HandleBorder.prototype = {
        constructor: HandleBorder,
        init: function(svgDoc) {
            this.currentSvgDoc = svgDoc;
            this.create();
            return this;
        },
    };

    HandleBorder.prototype.create = function() {
        var _this = this;

        _this.handleBorderGroup = _this.currentSvgDoc.group();

        _this.blockGroup = _this.handleBorderGroup.group();

        _this.rectLeftTop = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();
        _this.rectLeftBottom = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();
        _this.rectRightTop = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();
        _this.rectRightBottom = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();

        _this.rectLeftCenter = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();
        _this.rectRightCenter = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();
        _this.rectTopCenter = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();
        _this.rectBottomCenter = this.blockGroup.rect(sideLength, sideLength).stroke(sideWidth).draggable();


        var lastPoint = null;
        var xLeft;
        _this.rectLeftCenter.on("dragstart", function(event) {
            lastPoint = event.detail.p;
            xLeft = 1;
        });

        _this.rectLeftCenter.on("dragmove", function() {
            var currPoint = event.detail.p;
            var currPoint = event.detail.p;
            var dx = currPoint.x - lastPoint.x;

            var ele = _this.currentElement;
            var width = ele.width() - xLeft * dx;

            if (width > 0) {
                var newX = ele.x() + xLeft * dx;
                ele.x(newX).width(width);
            } else {
                //invert
                xLeft = -xLeft;
                ele.x(ele.bbox().x2).width(-width).matrix(new SVG.Matrix(ele).flip('x', ele.bbox().cx));
            }

            lastPoint = currPoint;
        });
        _this.rectLeftCenter.on("afterdragmove", function() {
            _this.rebound(_this.currentElement.bbox());
        });

    };


    HandleBorder.prototype.rebound = function(bbox) {
        var _this = this;

        var x1 = bbox.x;
        var y1 = bbox.y;
        var x2 = bbox.x2;
        var y2 = bbox.y2;
        _this.rectLeftTop.move(x1 - sideLength, y1 - sideLength);
        _this.rectLeftBottom.move(x1 - sideLength, y2);
        _this.rectRightTop.move(x2, y1 - sideLength);
        _this.rectRightBottom.move(x2, y2);

        _this.rectLeftCenter.move(x1 - sideLength, (y2 + y1 - sideLength) / 2);
        _this.rectRightCenter.move(x2, (y2 + y1 - sideLength) / 2);
        _this.rectTopCenter.move((x2 + x1 - sideLength) / 2, y1 - sideLength);
        _this.rectBottomCenter.move((x2 + x1 - sideLength) / 2, y2);

        this.blockGroup.matrix(new SVG.Matrix(_this.currentElement));
    };

    HandleBorder.prototype.show = function(svgEle) {
        if (!svgEle) {
            return;
        }
        this.currentElement = svgEle;
        this.handleBorderGroup.show();

        this.rebound(svgEle.bbox());
    };

    HandleBorder.prototype.hide = function() {
        this.handleBorderGroup.hide();
    };

    this.HandleBorder = HandleBorder;

})();

涉及到数学思维,总是绕不出去,只能不断的试错。

为每个操作框绑定事件太繁琐,是否可以通用?

0%