CrazyAirhead

疯狂的傻瓜,傻瓜也疯狂——傻方能执著,疯狂才专注!

0%

Web 开发 —— 基础

说明

Web 基础主要讲解 Solon 对 Web 开发提供的基础能力,支持 HTML 文档和其他静态资源的获取,提供数据交换的能力。

MVC

Web 开发中最常使用的 MVC 架构,MVC 全称是 “模型 (Model)- 视图 (View)- 控制器 (Controller)”,是一种广泛使用的设计模式,用于将应用程序的逻辑、数据和界面进行分离。

img

在具体对 MVC 的划分中可能存在一些边界的不同,有的人认为 Modle是输入,View 是输出,Controller是处理;有的人认为 Model 是数据,View 是数据的展示与用户的交互,Controller是处理,包含了输入和输出;有的人认为 Model 就是模型,即数据+接口,View 是模型的裁剪,Controller 是数据交互。随着前后端分离的架构的发展,前端也有自己的MVC 的结构,这个时候后端的接口的就成了 Model 层。根据自己对 MVC 的实际理解去用就好了,适合自己就好。

在 Solon 中可以这样理解:solon-data 对应处理 Model 层,提供数据处理、事务、缓存等支持;solon-core/solon-boot 对应处理 Controller 层,提供 Handler + Context 操作接口,提供Http信号接入支持;solon-view 对应处理 View 层(单体结构),提供不同的模板渲染框架,solon-serialization 对应处理 View 层(前后端分离结构),提供JSON、XML、PROTOSTUFF 等不同格式的序列化。

常用注解

@Controller 控制器注解(只有一个注解,会自动通过不同的返回值做不同的处理)
@Param 注入请求参数(包括:QueryString、Form、Path)。起到指定名字、默认值等作用
@Header 注入请求 header
@Cookie 注入请求 cookie
@Path 注入请求 path 变量(因为框架会自动处理,所以这个只是标识下方便文档生成用)
@Body 注入请求体(一般会自动处理。仅在主体的 String, Steam, Map 时才需要)
@Mapping 路由关系映射注解
@Get @Mapping 的辅助注解,便于 RESTful 开发
@Post @Mapping 的辅助注解,便于 RESTful 开发
@Put @Mapping 的辅助注解,便于 RESTful 开发
@Delete @Mapping 的辅助注解,便于 RESTful 开发
@Patch @Mapping 的辅助注解,便于 RESTful 开发
@Produces 输出内容类型申明
@Consumes 输入内容类型申明(当输出内容类型未包函 @Consumes,则响应为 415 状态码)
@Multipart 显式申明支持 Multipart 输入

参数注入

关于 MVC 参数注入,主要尊守以下规则:

  • 参数名与请求数据名一一对应(使用 -parameters)。
  • 当对映不上时,会采用整体数据注入(如果接收的是实体)
  • 参数名与请求数据同名时,又想整体注入(如果接收的是实体),可使用 @Body 注解强制标注。

个人偏好

不同的 MVC 的理解,会有不一样的项目结构,下面是阿里巴巴《Java 开发手册(黄山版)》的截图,可供参考。

img

img

以下属于个人偏好的目录结构,后续的示例会按这个方式处理。

1
2
3
4
5
6
7
domain(业务领域)
- controller(对应Contoller 层,提供用户交互接口)
- admin(管理端接口)
- app(应用端接)
- dto (对应 View 层的数据)
- model(对应 Model 层的数据)
- service(对应 Model 层的接口)

进一步补充,在 controller 中我不会放业务逻辑,只是调用 service进行转发,这样能更大程度的复用Service;在service中通常不采用接口实现的分离方式(虽然这种方式更好,但在编写、查看代码的过程中跳转太多,而且对应业务来说,通常不会有多种实现,有多种实现的逻辑再进行接口实现分离的方式);尽可能少的分层领域模型,避免过多的数据转换操作。

demo-web01

在 IDEA 中通过 File->New->Modules... 可以创建新的模块 demo-web01,基础代码和配置可以从demo-orm中拷贝过来,或者自己手动创建,之后调整包名。

这里我们继续补充demo-orm的部门管理的例子。实现基本的增删改查(CURD)接口,实现数据的导入导出展示文件的上传与下载,实现 index 接口用于展示模版的渲染,同时在模版中访问静态资源。提供 list 展示 json 的序列化,提供 listXml 展示 XML 的序列化。

为了在接口中尽可能多的展示不同的参数注入的使用,接口的定义不会是完全的 RESTful 风格。

修改依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
dependencies {
// enjoy 模版渲染
implementation("org.noear:solon-view-enjoy")

// xml 序列化
implementation("org.noear:solon-serialization-jackson-xml")

// json 序列化,默认就是snack3 可以不配置
implementation("org.noear:solon-serialization-snack3")

// 静态资源访问
implementation("org.noear:solon-web-staticfiles")
}

创建model

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
package com.example.demo.web.manage.model;

import com.easy.query.core.annotation.Column;
import com.easy.query.core.annotation.EntityProxy;
import com.easy.query.core.annotation.Table;
import com.easy.query.core.proxy.ProxyEntityAvailable;
import com.example.demo.web.manage.model.proxy.DeptEntityProxy;
import java.time.LocalDateTime;
import lombok.Data;

/**
* 部门表 实体类。
*
* @author Airhead
* @since 1.0
*/
@Data
@Table(value = "demo_dept")
@EntityProxy
public class DeptEntity implements ProxyEntityAvailable<DeptEntity, DeptEntityProxy> {

/** id */
@Column(primaryKey = true, value = "id", generatedKey = true)
private Long id;

/** 部门名称 */
private String name;

/** 部门编码 */
private String code;

/** 时间时间 */
private LocalDateTime createAt;

/** 更新时间 */
private LocalDateTime updateAt;
}

创建service

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
package com.example.demo.web.manage.service;

import com.easy.query.api.proxy.client.EasyEntityQuery;
import com.easy.query.solon.annotation.Db;
import com.example.demo.web.manage.convert.DeptConvert;
import com.example.demo.web.manage.dto.DeptDto;
import com.example.demo.web.manage.model.DeptEntity;
import java.util.List;
import org.noear.solon.annotation.*;

/**
* @author airhead
*/
@Component
public class DeptService {
@Db("db1")
private EasyEntityQuery entityQuery;

public List<DeptDto> list() {
return entityQuery.queryable(DeptEntity.class).select(DeptDto.class).toList();
}

public Boolean add(DeptDto deptDto) {
DeptEntity dept = DeptConvert.INSTANCE.convert(deptDto);

return entityQuery.insertable(dept).executeRows(true) > 0L;
}

public Boolean update(DeptDto deptDto) {
DeptEntity dept = DeptConvert.INSTANCE.convert(deptDto);
return entityQuery.updatable(dept).executeRows() > 0L;
}

public DeptDto get(Long id) {
return entityQuery
.queryable(DeptEntity.class)
.whereById(id)
.select(DeptDto.class)
.firstOrNull();
}

public Boolean delete(Long id) {
return entityQuery
.getEasyQueryClient()
.deletable(DeptEntity.class)
.allowDeleteStatement(true)
.whereById(id)
.executeRows()
> 0L;
}
}

创建dto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.example.demo.web.manage.dto;

import lombok.Data;

/**
* @author airhead
*/
@Data
public class DeptDto {
private Long id;

/** 部门名称 */
private String name;

/** 部门编码 */
private String code;
}

创建controller

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
package com.example.demo.web.manage.controller;

import com.example.demo.web.manage.dto.DeptDto;
import com.example.demo.web.manage.service.DeptService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.util.List;
import org.noear.solon.annotation.*;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.ModelAndView;
import org.noear.solon.core.handle.UploadedFile;

@Controller
@Mapping("/dept")
@Api("部门管理")
public class DeptController {
@Inject private DeptService service;

@Mapping(value = "list", produces = "application/json")
@Get
@ApiOperation("获取列表json")
public List<DeptDto> list() {
return service.list();
}

@Mapping(value = "listXml", produces = "application/xml")
@Get
@ApiOperation("获取列表Xml")
public List<DeptDto> listXml() {
return service.list();
}

@Mapping
@Post
@ApiOperation("新增")
public Boolean add(@Body DeptDto deptDto) {
return service.add(deptDto);
}

@Mapping
@Put
@ApiOperation("更新")
public Boolean update(@Body DeptDto deptDto) {
return service.update(deptDto);
}

@Mapping("/{id}")
@Get
@ApiOperation("获取")
public DeptDto get(@Path Long id) {
return service.get(id);
}

@Mapping
@Delete
@ApiOperation("删除")
public Boolean delete(@Param Long id) {
return service.delete(id);
}

@Mapping("/importData")
@Post
@ApiOperation("上传")
@Multipart
public Boolean importData(UploadedFile file) {
return service.importData(file);
}

@Mapping("/exportData")
@Get
@ApiOperation("下载")
public void exportData(Context context) {
service.exportData(context);
}

@Mapping("/index")
@Get
@ApiOperation("浏览部门")
public ModelAndView index() {
ModelAndView mv = new ModelAndView();
mv.put("depts", service.list());
mv.view("dept.shtm");

return mv;
}
}

上传下载

Solon 对上传下载的支持也是比较丰富的,这里只是使用了最常用的方式进行展示。更多的使用方法,参看 https://solon.noear.org/article/313

静态资源

引入solon-web-staticfiles可以提供公共的静态文件服务,默认静态文件目录是resources/static/ 。可以通过配置的的方式调整目录。

1
2
3
4
5
solon:
staticfiles:
mappings:
- path: "/img/"
repository: "classpath:img"

还可以通过代码的方式调整目录,更多的配置参看 https://solon.noear.org/article/268

验证

View

微信号的展示和logo,访问的是静态资源,且使用了不同的目录。

img

Json

img

Xml

img

上传

img

上传文件后,可以在日志中看到上传的文件名,可以在代码根目录看到对应上传的文件。

下载

下载的内容 swagger 工具显示不了,这里只展示返回的头信息。可以在浏览器中直接输入http://localhost:8080/dept/exportData 进行下载测试。

img

问题

在 Solon 3.0.7 版本测试的时候发现上述 controller 的路由定义,可能存在 index 接口无法访问,而路由到的 get 接口的情况,但 3.1.0-SNAPSHOT已经修复这个问题,当前代码实用了 Solon 3.1.0-SNAPSHOT,后续 3.1.0 发布时,教程也会统一更新版本号。

img

小结

MVC 虽然广为人知,但是在细节处还是存在理解的不同。Solon 对 MVC 用完整的支持,可以放心食用。不喜欢示例中的项目结构划分的可以参考阿里或者其他的目录结构进行项目结构的组织。

补充说明

春节期间的囤货已经用完了,也是第一次写比较完整的教程,后续教程的更新可能会比较慢。

欢迎关注我的其它发布渠道