正如前面所说,@JsonAnyGetter 和@JsonAnySetter 可能是您的最佳选择。
我认为你可以尽可能灵活地做到类型安全。
我想到的第一件事就是准确区分您需要的属性和其余的属性。
核心
Calculation.java
一个简单的不可变“计算”对象。
当然,它可以以任何其他方式设计,但我相信不变性使它更简单、更可靠。
final class Calculation {
private final double a;
private final double b;
private final Operation operation;
private final Double result;
private Calculation(final double a, final double b, final Operation operation, final Double result) {
this.a = a;
this.b = b;
this.operation = operation;
this.result = result;
}
static Calculation calculation(final double a, final double b, final Operation operation, final Double result) {
return new Calculation(a, b, operation, result);
}
Calculation calculate() {
return new Calculation(a, b, operation, operation.applyAsDouble(a, b));
}
double getA() {
return a;
}
double getB() {
return b;
}
Operation getOperation() {
return operation;
}
Double getResult() {
return result;
}
}
Operation.java
一个简单的在枚举中定义的计算策略,因为 Jackson 使用枚举非常好。
enum Operation
implements DoubleBinaryOperator {
ADD {
@Override
public double applyAsDouble(final double a, final double b) {
return a + b;
}
},
SUBTRACT {
@Override
public double applyAsDouble(final double a, final double b) {
return a - b;
}
},
MULTIPLY {
@Override
public double applyAsDouble(final double a, final double b) {
return a * b;
}
},
DIVIDE {
@Override
public double applyAsDouble(final double a, final double b) {
return a / b;
}
}
}
杰克逊映射
AbstractTask.java
请注意,该类旨在提供一个值,但将其余部分收集到卫星地图中,该卫星地图由使用@JsonAnySetter 和@JsonAnyGetter 注释的方法管理。
地图和方法可以安全地声明为private,因为杰克逊并不真正关心保护级别(这很棒)。
此外,它以不可变的方式设计,除了可以浅拷贝到新值的底层映射。
abstract class AbstractTask<V>
implements Supplier<V> {
@JsonIgnore
private final Map<String, Object> rest = new LinkedHashMap<>();
protected abstract AbstractTask<V> toTask(V value);
final <T extends AbstractTask<V>> T with(final V value) {
final AbstractTask<V> dto = toTask(value);
dto.rest.putAll(rest);
@SuppressWarnings("unchecked")
final T castDto = (T) dto;
return castDto;
}
@JsonAnySetter
@SuppressWarnings("unused")
private void set(final String name, final Object value) {
rest.put(name, value);
}
@JsonAnyGetter
@SuppressWarnings("unused")
private Map<String, Object> getRest() {
return rest;
}
}
CalculationTask.java
这是一个定义具体计算任务的类。
同样,Jackson 与私有字段和方法完美配合,因此可以封装整个复杂性。
我可以看到的一个缺点是 JSON 属性被声明为用于序列化和反序列化,但它也可以被认为是一个优点。
请注意,@JsonGetter 参数在这里不是必需的,但我只是将属性名称加倍以用于输入和输出操作。
没有任何任务打算手动实例化 - 让 Jackson 来做吧。
final class CalculationTask
extends AbstractTask<Calculation> {
private final Calculation calculation;
private CalculationTask(final Calculation calculation) {
this.calculation = calculation;
}
@JsonCreator
@SuppressWarnings("unused")
private static CalculationTask calculationTask(
@JsonProperty("a") final double a,
@JsonProperty("b") final double b,
@JsonProperty("operation") final Operation operation,
@JsonProperty("result") final Double result
) {
return new CalculationTask(calculation(a, b, operation, result));
}
@Override
public Calculation get() {
return calculation;
}
@Override
protected AbstractTask<Calculation> toTask(final Calculation calculation) {
return new CalculationTask(calculation);
}
@JsonGetter("a")
@SuppressWarnings("unused")
private double getA() {
return calculation.getA();
}
@JsonGetter("b")
@SuppressWarnings("unused")
private double getB() {
return calculation.getB();
}
@JsonGetter("operation")
@SuppressWarnings("unused")
private Operation getOperation() {
return calculation.getOperation();
}
@JsonGetter("result")
@SuppressWarnings("unused")
private Double getResult() {
return calculation.getResult();
}
}
客户端-服务器交互
CalculationController.java
这是一个简单的 GET/PUT/DELETE 控制器,用于集成测试,或者只是用 curl 手动测试。
@RestController
@RequestMapping("/")
final class CalculationController {
private final CalculationService processService;
@Autowired
@SuppressWarnings("unused")
CalculationController(final CalculationService processService) {
this.processService = processService;
}
@RequestMapping(method = GET, value = "{id}")
@ResponseStatus(OK)
@SuppressWarnings("unused")
CalculationTask get(@PathVariable("id") final String id) {
return processService.get(id);
}
@RequestMapping(method = PUT, value = "{id}")
@ResponseStatus(NO_CONTENT)
@SuppressWarnings("unused")
void put(@PathVariable("id") final String id, @RequestBody final CalculationTask task) {
processService.put(id, task);
}
@RequestMapping(method = DELETE, value = "{id}")
@ResponseStatus(NO_CONTENT)
@SuppressWarnings("unused")
void delete(@PathVariable("id") final String id) {
processService.delete(id);
}
}
ControllerExceptionHandler.java
由于下面在 DAO 类中声明的 get 和 delete 方法抛出 NoSuchElementException,因此可以轻松地将异常映射到 HTTP 404。
@ControllerAdvice
final class ControllerExceptionHandler {
@ResponseStatus(NOT_FOUND)
@ExceptionHandler(NoSuchElementException.class)
@SuppressWarnings("unused")
void handleNotFound() {
}
}
应用程序本身
CalculationService.java
只是一个包含一些“业务”逻辑的简单服务。
@Service
final class CalculationService {
private final CalculationDao processDao;
@Autowired
CalculationService(final CalculationDao processDao) {
this.processDao = processDao;
}
CalculationTask get(final String id) {
return processDao.get(id);
}
void put(final String id, final CalculationTask task) {
processDao.put(id, task.with(task.get().calculate()));
}
void delete(final String id) {
processDao.delete(id);
}
}
数据层
CalculationMapping.java
只是一个持有者类,以便在 Spring Data 中使用 MongoDB 存储库,指定目标 MongoDB 文档集合名称。
@Document(collection = "calculations")
public final class CalculationTaskMapping
extends org.bson.Document {
@Id
@SuppressWarnings("unused")
private String id;
}
ICalculationRepository.java
CalculationMapping 类的 Spring Data MongoDB CRUD 存储库。
下面使用这个存储库。
@Repository
interface ICalculationRepository
extends MongoRepository<CalculationTaskMapping, String> {
}
CalculationDao.java
DAO 组件在演示本身并没有做太多的工作,更多的是把持久化工作委托给它的超类,并且很容易被 Spring Framework 找到。
@Component
final class CalculationDao
extends AbstractDao<CalculationTask, CalculationTaskMapping, String> {
@Autowired
CalculationDao(@SuppressWarnings("TypeMayBeWeakened") final ICalculationRepository calculationRepository, final ObjectMapper objectMapper) {
super(CalculationTaskMapping.class, calculationRepository, objectMapper);
}
}
AbstractDao.java
这是持久化整个原始对象的核心。
ObjectMapper 实例用于根据 Jackson 注释指定的序列化规则将任务转换为它们各自的任务映射(参见 convertValue 方法)。
由于演示使用 Spring Data MongoDB,映射类实际上是 Map<String, Object> 并继承 Document 类。
不幸的是,面向 Map 的映射似乎不适用于 Spring Data MongoDB 注释,如 @Id、@Field 等(更多信息请参阅 How do I combine java.util.Map-based mappings with the Spring Data MongoDB annotations (@Id, @Field, ...)?)。
但是,只要您不想映射任意文档,它就可以证明是合理的。
abstract class AbstractDao<T, M extends Document, ID extends Serializable> {
private final Class<M> mappingClass;
private final CrudRepository<M, ID> crudRepository;
private final ObjectMapper objectMapper;
protected AbstractDao(final Class<M> mappingClass, final CrudRepository<M, ID> crudRepository, final ObjectMapper objectMapper) {
this.mappingClass = mappingClass;
this.crudRepository = crudRepository;
this.objectMapper = objectMapper;
}
final void put(final ID id, final T task) {
final M taskMapping = objectMapper.convertValue(task, mappingClass);
taskMapping.put(ID_FIELD_NAME, id);
if ( crudRepository.exists(id) ) {
crudRepository.delete(id);
}
crudRepository.save(taskMapping);
}
final CalculationTask get(final ID id) {
final Map<String, Object> rawTask = crudRepository.findOne(id);
if ( rawTask == null ) {
throw new NoSuchElementException();
}
rawTask.remove(ID_FIELD_NAME);
return objectMapper.convertValue(rawTask, CalculationTask.class);
}
final void delete(final ID id) {
final M taskMapping = crudRepository.findOne(id);
if ( taskMapping == null ) {
throw new NoSuchElementException();
}
crudRepository.delete(id);
}
}
Spring Boot 应用程序
EntryPoint.class
还有一个 Spring Boot 演示,它将所有这些作为一个监听端口 9000 的单个 HTTP 应用程序运行。
@SpringBootApplication
@Configuration
@EnableWebMvc
public class EntryPoint
extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(final SpringApplicationBuilder builder) {
return builder.sources(EntryPoint.class);
}
@Bean
EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer() {
return c -> c.setPort(9000);
}
@Bean
ObjectMapper objectMapper() {
return new ObjectMapper()
.setSerializationInclusion(NON_NULL)
.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
}
@SuppressWarnings("resource")
public static void main(final String... args) {
SpringApplication.run(EntryPoint.class, args);
}
}
使用curl 测试应用程序
(mongodb-shell)
> use test
切换到本地数据库
(bash)
$ curl -v -X GET http://localhost:9000/foo
> GET /foo HTTP/1.1
> 用户代理:curl/7.35.0
> 主机:本地主机:9000
> 接受:/
>
(mongodb-shell)
> db.calculations.find()
(空)
(bash)
$ curl -v -X PUT -H 'Content-Type: application/json' \
--data '{"a":3,"b":4,"operation":"MULTIPLY","result":12,"foo":"FOO","bar":"BAR"}' \
http://localhost:9000/foo
> PUT /foo HTTP/1.1
> 用户代理:curl/7.35.0
> 主机:本地主机:9000
> 接受:/
> 内容类型:application/json
> 内容长度:72
>
(mongodb-shell)
> db.calculations.find()
{“_id”:“foo”,“_class”:“q41036545.CalculationTaskMapping”,“a”:3,“b”:4,“操作”:“MULTIPLY”,“结果”:12,“foo " : "FOO", "bar" : "BAR" }
(bash)
$ curl -v -X GET http://localhost:9000/foo
> GET /foo HTTP/1.1
> 用户代理:curl/7.35.0
> 主机:本地主机:9000
> 接受:/
>
{"a":3.0,"b":4.0,"operation":"MULTIPLY","result":12.0,"foo":"FOO","bar":"BAR"}
(bash)
curl -v -X DELETE http://localhost:9000/foo
> 删除 /foo HTTP/1.1
> 用户代理:curl/7.35.0
> 主机:本地主机:9000
> 接受:/
>
(mongodb-shell)
> db.calculations.find()
(空)
源代码可以在https://github.com/lyubomyr-shaydariv/q41036545找到