第一次接触MVC是在2008年参与一个电商后台重构项目,当时系统前后端代码混杂得像一碗意大利面。当我看到同事用Ruby on Rails将代码按MVC拆分后,那种清晰的边界感让我至今难忘。MVC不是银弹,但绝对是软件工程史上最成功的架构模式之一。
简单来说,MVC把应用分为三个核心部件:
这种分离带来的直接好处是:当需要修改界面时,你不需要碰业务逻辑;调整业务规则时,也不用担心会破坏界面。就像装修房子时,水电管线(Model)和墙面装饰(View)是独立施工的,而设计师(Controller)负责协调两者。
Model远不止是数据库表的映射。在最近开发的物流系统中,我们的OrderModel包含了:
一个常见的误区是把Model写成贫血模型。实际上,好的Model应该像这样包含丰富行为:
java复制public class Order {
private String status;
public void cancel() {
validateCancelable();
this.status = "CANCELLED";
notifyLogistics();
refundPayment();
}
private void validateCancelable() {
if(!"PENDING".equals(status)) {
throw new IllegalStateException("仅待支付订单可取消");
}
}
}
从JSP时代到现在的React/Vue,View技术发生了翻天覆地的变化。但核心原则不变:View应当:
在现代前端框架中,我们常使用MVVM模式(本质是MVC的变种)。比如Vue组件:
html复制<template>
<!-- 纯展示逻辑 -->
<div v-if="order.status === 'PENDING'">
<button @click="cancelOrder">取消订单</button>
</div>
</template>
<script>
export default {
methods: {
// 事件触发后直接委托给Controller
cancelOrder() {
this.$emit('cancel-order');
}
}
}
</script>
早期Struts时代的Controller经常变成上帝类。现代最佳实践是:
Spring Boot中的典型Controller:
java复制@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
// 依赖注入
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/{id}/cancel")
public ResponseEntity<Void> cancelOrder(
@PathVariable Long id,
@CurrentUser User user) {
// 仅做参数校验和转发
if (id == null) {
return ResponseEntity.badRequest().build();
}
orderService.cancelOrder(id, user);
return ResponseEntity.noContent().build();
}
}
根据应用复杂度不同,我通常采用这三种模式:
基础MVC(适合简单CRUD)
服务层增强型(中型应用)
mermaid复制graph TD
View --> Controller
Controller --> Service
Service --> Repository
Repository --> Model
领域驱动设计型(复杂业务)
经验法则:当Controller方法超过300行,就该考虑引入Service层了
直接调用(最简单但耦合度高)
java复制// Controller中直接调用DAO
userDao.save(newUser);
事件驱动(解耦但复杂度高)
java复制// 订单创建后发布领域事件
eventPublisher.publish(new OrderCreatedEvent(order));
DTO模式(前后端分离场景)
java复制// 返回专用的数据传输对象
@GetMapping("/users/{id}")
public UserDTO getUser(@PathVariable Long id) {
User user = userService.getById(id);
return new UserDTO(user);
}
胖控制器综合症
贫血模型蔓延
视图逻辑泄露
在电商下单场景中,错误的做法是:
java复制// Controller中直接管理事务
@Transactional
public void placeOrder(OrderForm form) {
// 数十行业务逻辑...
}
正确的分层应该是:
java复制// Service层方法
@Transactional
public Order placeOrder(OrderCommand command) {
Order order = createOrder(command);
processPayment(order);
scheduleDelivery(order);
return order;
}
// Controller仅做参数转换
@PostMapping("/orders")
public OrderDTO createOrder(@RequestBody OrderForm form) {
OrderCommand command = convertToCommand(form);
Order order = orderService.placeOrder(command);
return convertToDTO(order);
}
Spring Boot通过自动配置简化了MVC搭建:
@RestController标记Controllerspring-boot-starter-web包含嵌入式Tomcat但要注意这些隐藏约定:
/static目录/templates在现代SPA应用中,MVC演变为:
接口设计建议:
java复制@GetMapping("/api/products")
public Page<ProductDTO> getProducts(
@RequestParam(required = false) String keyword,
@PageableDefault Pageable pageable) {
return productService.search(keyword, pageable);
}
Model层测试(占比60%)
java复制@Test
void should_throw_when_cancel_paid_order() {
Order order = new Order("PAID");
assertThatThrownBy(order::cancel)
.isInstanceOf(IllegalStateException.class);
}
Controller层测试(占比30%)
java复制mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("张三"));
View层测试(占比10%)
javascript复制test('should show login form', () => {
render(<Login />);
expect(screen.getByLabelText('用户名')).toBeInTheDocument();
});
使用Testcontainers进行真实数据库测试:
java复制@Testcontainers
class OrderIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>();
@Test
void should_persist_order() {
// 配置数据源连接容器数据库
OrderRepository repo = new JdbcOrderRepository(
mysql.getJdbcUrl(),
mysql.getUsername(),
mysql.getPassword());
Order order = new Order("PENDING");
repo.save(order);
assertThat(repo.findById(order.getId()))
.isPresent();
}
}
典型症状:获取订单列表时,每条订单又单独查询用户信息。
解决方案:
JOIN FETCH(JPA)
java复制@Query("SELECT o FROM Order o JOIN FETCH o.user")
List<Order> findAllWithUser();
批量加载(MyBatis)
xml复制<resultMap id="orderWithUser" type="Order">
<association property="user" column="user_id"
select="com.example.mapper.UserMapper.findById"/>
</resultMap>
<select id="findAll" resultMap="orderWithUser">
SELECT * FROM orders
</select>
查询缓存(Spring Cache)
java复制@Cacheable("products")
public Product getProduct(Long id) {
return productRepository.findById(id);
}
DTO缓存(Redis)
java复制public ProductDTO getProductDTO(Long id) {
String key = "product:" + id;
return redisTemplate.opsForValue()
.computeIfAbsent(key, k -> {
Product product = getProduct(id);
return convertToDTO(product);
});
}
静态资源缓存(HTTP缓存头)
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
}
}
随着业务复杂度的增长,传统MVC会显现出局限性。这时可以考虑:
六边形架构:
领域驱动设计:
迁移示例:
java复制// 传统MVC
@Controller
public class OrderController {
private OrderRepository orderRepo;
@PostMapping("/orders")
public String create(OrderForm form) {
Order order = convert(form);
orderRepo.save(order);
return "redirect:/orders";
}
}
// DDD风格
@RestController
public class OrderCommandController {
private OrderApplicationService appService;
@PostMapping("/api/orders")
public ResponseEntity<Void> create(@RequestBody CreateOrderCommand cmd) {
String orderId = appService.createOrder(cmd);
return ResponseEntity.created(URI.create("/api/orders/" + orderId)).build();
}
}
推荐按功能模块组织(优于按技术分层):
code复制src/
├── order/
│ ├── application/ # 用例层
│ ├── domain/ # 领域层
│ ├── infrastructure/ # 基础设施
│ └── interface/ # 接口层
└── user/
├── application/
├── domain/
└── interface/
资源命名:
/orders而非/getOrderList状态码规范:
版本控制:
java复制@RestController
@RequestMapping("/api/v1/orders")
public class OrderControllerV1 {
// ...
}
Controller(订单接口):
java复制@PostMapping("/orders")
public ResponseEntity<OrderResult> createOrder(
@Valid @RequestBody OrderRequest request) {
OrderCommand command = converter.toCommand(request);
Order order = orderService.createOrder(command);
return ResponseEntity.created(
URI.create("/orders/" + order.getId()))
.body(converter.toResult(order));
}
Service(领域逻辑):
java复制@Transactional
public Order createOrder(OrderCommand command) {
// 验证库存
inventoryService.checkStock(command.getItems());
// 创建订单聚合根
Order order = new Order(command.getUserId());
order.addItems(command.getItems());
// 支付处理
paymentService.process(order);
// 持久化
return orderRepository.save(order);
}
Model(领域对象):
java复制public class Order {
private List<OrderItem> items;
public void addItems(List<OrderItem> items) {
if (items == null || items.isEmpty()) {
throw new IllegalArgumentException("订单项不能为空");
}
this.items.addAll(items);
calculateTotal();
}
private void calculateTotal() {
this.total = items.stream()
.mapToDouble(i -> i.getPrice() * i.getQuantity())
.sum();
}
}
使用Spring StateMachine处理支付流程:
java复制@Configuration
@EnableStateMachineFactory
public class PaymentStateMachineConfig {
@Bean
public StateMachine<PaymentState, PaymentEvent> stateMachine() {
StateMachineBuilder.Builder<PaymentState, PaymentEvent> builder = StateMachineBuilder.builder();
builder.configureStates()
.withStates()
.initial(PaymentState.PENDING)
.state(PaymentState.PROCESSING)
.end(PaymentState.COMPLETED)
.end(PaymentState.FAILED);
builder.configureTransitions()
.withExternal()
.source(PaymentState.PENDING)
.target(PaymentState.PROCESSING)
.event(PaymentEvent.START_PAYMENT)
.and()
.withExternal()
.source(PaymentState.PROCESSING)
.target(PaymentState.COMPLETED)
.event(PaymentEvent.PAYMENT_SUCCESS)
.and()
.withExternal()
.source(PaymentState.PROCESSING)
.target(PaymentState.FAILED)
.event(PaymentEvent.PAYMENT_FAILED);
return builder.build();
}
}
同步调用(FeignClient):
java复制@FeignClient(name = "inventory-service")
public interface InventoryClient {
@PostMapping("/api/inventory/check")
void checkStock(@RequestBody List<OrderItem> items);
}
异步事件(Spring Cloud Stream):
java复制@Service
@RequiredArgsConstructor
public class OrderEventPublisher {
private final StreamBridge streamBridge;
public void publishOrderCreated(Order order) {
streamBridge.send("orderCreated-out-0",
new OrderCreatedEvent(order.getId()));
}
}
对于需要跨服务数据的场景:
java复制@RestController
@RequiredArgsConstructor
public class OrderCompositeController {
private final OrderClient orderClient;
private final UserClient userClient;
@GetMapping("/composite/orders/{id}")
public CompositeOrder getCompositeOrder(@PathVariable String id) {
Order order = orderClient.getOrder(id);
User user = userClient.getUser(order.getUserId());
return CompositeOrder.builder()
.order(order)
.user(user)
.build();
}
}
使用Sleuth+Zipkin记录请求流:
java复制@RestController
public class OrderController {
private final Tracer tracer;
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable String id) {
Span span = tracer.nextSpan().name("dbQuery");
try (Scope scope = tracer.withSpan(span)) {
return orderRepository.findById(id);
} finally {
span.end();
}
}
}
Spring Boot Actuator配置:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
metrics:
enabled: true
JWT实现:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthFilter(authenticationManager()));
}
}
方法级权限控制:
java复制@PreAuthorize("hasRole('ADMIN') or #userId == principal.id")
@GetMapping("/users/{userId}/orders")
public List<Order> getUserOrders(@PathVariable Long userId) {
return orderService.findByUserId(userId);
}
Bean Validation:
java复制@PostMapping("/products")
public void createProduct(@Valid @RequestBody ProductCreateRequest request) {
productService.create(request);
}
public class ProductCreateRequest {
@NotBlank
private String name;
@Positive
private BigDecimal price;
}
自定义验证器:
java复制@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
public @interface ValidPhoneNumber {
String message() default "Invalid phone number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
定义消息文件:
properties复制# messages.properties
order.not_found=Order not found: {0}
# messages_zh_CN.properties
order.not_found=订单不存在: {0}
控制器中使用:
java复制@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable String id, Locale locale) {
return orderRepo.findById(id)
.orElseThrow(() -> new ResponseStatusException(
NOT_FOUND,
messageSource.getMessage(
"order.not_found",
new Object[]{id},
locale)));
}
配置全局Jackson序列化:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
converters.add(new MappingJackson2HttpMessageConverter(mapper));
}
}
使用SpringDoc生成Swagger文档:
java复制@OpenAPIDefinition(
info = @Info(
title = "订单服务API",
version = "1.0",
description = "订单管理相关接口"
),
servers = @Server(url = "/api")
)
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("bearerAuth",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}
使用Spring Rest Docs生成示例:
java复制@Test
void getOrderExample() throws Exception {
mockMvc.perform(get("/api/orders/{id}", "123")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("get-order",
pathParameters(
parameterWithName("id").description("订单ID")),
responseFields(
fieldWithPath("id").description("订单编号"),
fieldWithPath("status").description("订单状态"))));
}
分层构建示例:
dockerfile复制# 构建阶段
FROM maven:3.8-jdk-11 as builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
# 运行阶段
FROM openjdk:11-jre-slim
COPY --from=builder /app/target/*.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]
Deployment配置示例:
yaml复制apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
spec:
containers:
- name: app
image: registry.example.com/orders:v1.2.0
ports:
- containerPort: 8080
env:
- name: DB_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
自动化构建部署:
yaml复制name: CI/CD Pipeline
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: '11'
- name: Build with Maven
run: mvn package -DskipTests
- name: Build Docker image
run: docker build -t orders:${{ github.sha }} .
- name: Login to Registry
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Push Image
run: |
docker tag orders:${{ github.sha }} registry.example.com/orders:${{ github.sha }}
docker push registry.example.com/orders:${{ github.sha }}
SonarQube集成示例:
xml复制<!-- pom.xml -->
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.9.1</version>
</plugin>
yaml复制# GitHub Actions步骤
- name: SonarQube Scan
run: mvn sonar:sonar -Dsonar.login=${{ secrets.SONAR_TOKEN }}
典型电商应用配置:
bash复制java -server \
-Xms2g -Xmx2g \
-XX:MaxMetaspaceSize=512m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:ParallelGCThreads=4 \
-XX:ConcGCThreads=2 \
-jar app.jar
HikariCP最佳实践:
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 30000
max-lifetime: 1800000
connection-timeout: 30000
pool-name: OrderHikariPool
迁移步骤建议:
基于Spring的抽象:
java复制@Configuration
@EnableTransactionManagement
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource routingDataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource());
targetDataSources.put("slave", slaveDataSource());
RoutingDataSource routingDataSource = new RoutingDataSource();
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(masterDataSource());
return routingDataSource;
}
}
识别边界:先剥离相对独立的模块
代理路由:用网关逐步迁移流量
java复制@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("legacy-order", r -> r.path("/legacy/orders/**")
.uri("http://old-system:8080"))
.route("new-order", r -> r.path("/api/orders/**")
.uri("lb://order-service"))
.build();
}
最终替换:当新系统覆盖全部功能后下线旧系统
使用Flyway分阶段迁移:
sql复制-- V1__create_orders_table.sql
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL
);
-- V2__add_order_status.sql
ALTER TABLE orders ADD COLUMN status VARCHAR(20);
应用指标:
JVM指标:
数据库指标:
Arthas(线上诊断):
bash复制# 监控方法调用
watch com.example.service.OrderService createOrder '{params, returnObj}' -x 3
Async Profiler(CPU分析):
bash复制./profiler.sh -d 30 -f profile.html <pid>
JDK工具:
bash复制jstat -gcutil <pid> 1000
jstack <pid> > thread_dump.txt
使用ChaosBlade模拟异常:
bash复制# 模拟数据库延迟
blade create mysql delay --time 3000 --offset 1000 --table orders --sqltype select
# 模拟方法抛出异常
blade create method throwCustomException --classname com.example.OrderService --methodname createOrder --exception java.lang.RuntimeException
熔断机制(Resilience4j):
java复制@CircuitBreaker(name = "inventoryService", fallbackMethod = "fallbackCheck")
public void checkInventory(Order order) {
inventoryClient.check(order.getItems());
}
private void fallbackCheck(Order order, Exception e) {
log.warn("库存检查降级", e);
}
重试策略:
java复制@Retry(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResult processPayment(PaymentRequest request) {
return paymentClient.process(request);
}
订单聚合示例:
java复制public class Order {
private OrderId id;
private List<OrderItem> items;
private Payment payment;
public void addItem(Product product, int quantity) {
// 保持聚合内一致性
if (payment != null) {
throw new IllegalStateException("已支付订单不能修改");
}
items.add(new OrderItem(product, quantity));
}
public void applyPayment(Payment payment) {
// 验证支付金额匹配
if (!payment.getAmount().equals(calculateTotal())) {
throw new IllegalArgumentException("支付金额不匹配");
}
this.payment = payment;
}
}
订单领域事件:
java复制public class OrderPaidEvent {
private OrderId orderId;
private PaymentId paymentId;
private LocalDateTime paidAt;
public static OrderPaidEvent of(Order order) {
return new OrderPaidEvent(
order.getId(),
order.getPayment().getId(),
LocalDateTime.now());
}
}
// 事件处理器
@Service
@TransactionalEventListener(phase = AFTER_COMMIT)
public class OrderPaidHandler {
private final NotificationService notificationService;
public void handle(OrderPaidEvent event) {
notificationService.sendPaymentReceipt(event.orderId());
}
}
Spring实现示例:
java复制// 写端
@RestController
@RequestMapping("/api/orders")
public class OrderCommandController {
private final OrderCommandService commandService;
@PostMapping
public ResponseEntity<Void> create(@RequestBody CreateOrderCommand cmd) {
String orderId = commandService.createOrder(cmd);
return ResponseEntity.created(URI.create("/api/orders/" + orderId)).build();
}
}
// 读端
@RestController
@RequestMapping("/api/orders")
public class OrderQueryController {
private final OrderQueryService queryService;
@GetMapping("/{id}")
public OrderDTO getOrder(@PathVariable String id) {
return queryService.getOrder(id);
}
}
使用Axon Framework:
java复制@Aggregate
public class OrderAggregate {
@AggregateIdentifier
private String orderId;
@CommandHandler
public OrderAggregate(CreateOrderCommand cmd) {
apply(new OrderCreatedEvent(cmd.getOrderId(), cmd.getItems()));
}
@EventSourcingHandler
public void on(OrderCreatedEvent event) {
this.orderId = event.getOrderId();
// 初始化状态...
}
}
响应式Controller示例:
java复制@RestController
@RequestMapping("/reactive/orders")
public class ReactiveOrderController {
private final ReactiveOrderService orderService;
@GetMapping("/{id}")
public Mono<OrderDTO> getOrder(@PathVariable String id) {
return orderService.findById(id);
}
@PostMapping
public Mono<ResponseEntity<Void>> create(@RequestBody Mono<CreateOrderRequest> request) {
return request.flatMap(orderService::create)
.map(id -> ResponseEntity.created(URI.create("/orders/" + id)).build());
}
}
响应式Repository:
java复制public interface ReactiveOrderRepository extends R2dbcRepository<Order, Long> {
@Query("SELECT * FROM orders WHERE user_id = :userId")
Flux<Order> findByUserId(Long userId);
@Modifying
@Query("UPDATE orders SET status = :status WHERE id = :id")
Mono<Integer> updateStatus(@Param("id") Long id, @Param("status") String status);
}
订单服务Schema示例:
graphql复制type Order {
id: ID!
items: [OrderItem!]!
total: Float!
status: OrderStatus!
}
type Query {
order(id: ID!): Order
orders(userId: ID, status: OrderStatus): [Order!]!
}
input CreateOrderInput {
items: [OrderItemInput!]!
}
type Mutation {
createOrder(input: CreateOrderInput!): Order!
}
Spring GraphQL实现:
java复制@Controller
public class OrderGraphQLController {
private final OrderService orderService;
@QueryMapping
public Order order(@Argument String id) {
return orderService.findById(id);
}
@MutationMapping
public Order createOrder(@Argument CreateOrderInput input) {
return orderService.create(input);
}
@SchemaMapping
public List<OrderItem> items(Order order) {
return orderService.getItems(order.getId());
}
}
订单处理函数:
java复制public class OrderHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
private final ObjectMapper mapper = new ObjectMapper();
private final OrderService orderService = new OrderService();
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
try {
CreateOrderRequest request = mapper.readValue(input.getBody(), CreateOrderRequest.class);
String orderId = orderService.createOrder(request);
return new APIGatewayProxyResponseEvent()
.withStatusCode(201)
.withBody("{\"orderId\":\"" + orderId + "\"}");
} catch (Exception e) {
return new APIGatewayProxyResponseEvent()
.withStatusCode(500)
.withBody("{\"error\":\"" + e.getMessage() + "\"}");
}
}
}
函数式Web端点:
java复制@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
@Bean
public Function<CreateOrderRequest, OrderResult> createOrder(Order