我的第一个上线项目:从"本地王者"到"服务器乞丐"
我的第一个上线项目:从"本地王者"到"服务器乞丐"
"为什么生产环境显示不出来?我本地明明好好的!"凌晨两点,我在空荡荡的办公室里对着屏幕咆哮。距离产品上线还有6小时,前端页面一片空白,而我连Nginx是什么都不知道。从开发到上线,原来隔着十万八千个坑。

一、本地环境搭建:我以为的"简单"其实不简单
1.1 那个让我怀疑人生的跨域问题
我按照教程写完了所有代码,前端访问后端接口时:

text
Access to XMLHttpRequest at 'http://localhost:8080/employee/login'
from origin 'http://localhost:5500' has been blocked by CORS policy
我的"解决方案":在Controller上疯狂加注解:
java
// 版本1:乱加注解
@CrossOrigin(origins = "*", allowedHeaders = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.OPTIONS, RequestMethod.PATCH, RequestMethod.HEAD, RequestMethod.TRACE})
@RestController
@RequestMapping("/employee")
public class EmployeeController {
// ...
}
还是不行!为什么?
真正的问题:我的过滤器拦截了所有请求,包括OPTIONS预检请求,但过滤器里没有放行OPTIONS方法。
正确的解决方案:
java
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 1. 放行OPTIONS预检请求
if ("OPTIONS".equals(request.getMethod())) {
chain.doFilter(request, response);
return;
}
// 2. 白名单
String[] freeUrls = {
"/employee/login", "/employee/logout",
"/backend/**", "/front/**"
};
String requestURI = request.getRequestURI();
boolean isFree = check(freeUrls, requestURI);
if (isFree) {
chain.doFilter(request, response);
return;
}
// 3. 检查登录状态
if (request.getSession().getAttribute("employee") != null) {
chain.doFilter(request, response);
return;
}
// 4. 未登录
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
}
private boolean check(String[] urls, String requestURI) {
AntPathMatcher pathMatcher = new AntPathMatcher();
for (String url : urls) {
if (pathMatcher.match(url, requestURI)) {
return true;
}
}
return false;
}
}
1.2 那个让我熬夜的静态资源路径问题
我的前端页面打开是一片空白,控制台报错:
text
GET http://localhost:8080/backend/css/style.css 404
我检查了代码,静态资源明明在resources/backend/css/style.css啊!
问题:Spring Boot默认的静态资源路径是/static、/public、/resources、/META-INF/resources,而我把资源放在了resources/backend,这不在默认扫描路径内。
解决方案:配置静态资源映射
java
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始静态资源映射...");
// 映射backend路径
registry.addResourceHandler("/backend/**")
.addResourceLocations("classpath:/backend/");
// 映射front路径(用户端)
registry.addResourceHandler("/front/**")
.addResourceLocations("classpath:/front/");
}
}
二、数据库设计:从"能用就行"到"优化性能"

2.1 我的第一个员工表设计
sql
-- 我的"初版"员工表
CREATE TABLE employee (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
username VARCHAR(20),
password VARCHAR(20),
phone VARCHAR(11),
sex VARCHAR(2),
id_number VARCHAR(18),
status INT,
create_time DATETIME,
update_time DATETIME,
create_user BIGINT,
update_user BIGINT
);
看起来没问题?导师指出了几个问题:
- 主键问题:
INT最大21亿,对于大型系统可能不够 - 密码明文存储:
VARCHAR(20)存明文密码,安全隐患 - 索引缺失:没有为
username和phone等常用查询字段加索引 - 性别存储:用
VARCHAR(2)存储"男"/"女",浪费空间且不标准 - 时间字段:没有默认值,需要手动设置
2.2 优化后的数据库设计
sql
-- 优化后的员工表
CREATE TABLE `employee` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(32) COLLATE utf8mb4_bin NOT NULL COMMENT '姓名',
`username` varchar(32) COLLATE utf8mb4_bin NOT NULL COMMENT '用户名',
`password` varchar(64) COLLATE utf8mb4_bin NOT NULL COMMENT '密码',
`phone` varchar(11) COLLATE utf8mb4_bin NOT NULL COMMENT '手机号',
`sex` varchar(2) COLLATE utf8mb4_bin NOT NULL COMMENT '性别',
`id_number` varchar(18) COLLATE utf8mb4_bin NOT NULL COMMENT '身份证号',
`status` int NOT NULL DEFAULT '1' COMMENT '状态 0:禁用,1:正常',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`create_user` bigint NOT NULL COMMENT '创建人',
`update_user` bigint NOT NULL COMMENT '修改人',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `idx_username` (`username`) COMMENT '用户名唯一索引',
KEY `idx_phone` (`phone`) COMMENT '手机号索引',
KEY `idx_status` (`status`) COMMENT '状态索引',
KEY `idx_create_time` (`create_time`) COMMENT '创建时间索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='员工信息';
改进点:
- 主键:
bigint支持最多922亿亿条记录 - 密码:
varchar(64),可以存储MD5加密后的32位hex字符串 - 索引:为查询频繁的字段添加索引
- 默认值:
status默认1(正常),时间字段自动设置 - 字符集:
utf8mb4支持表情符号
2.3 初始化数据脚本
sql
-- 初始化管理员账户
INSERT INTO `employee`
(`id`, `name`, `username`, `password`, `phone`, `sex`, `id_number`, `status`, `create_user`, `update_user`)
VALUES
(1, '管理员', 'admin', 'e10adc3949ba59abbe56e057f20f883e', '13812345678', '1', '110101199001011234', 1, 1, 1);
-- 密码:123456的MD5加密
三、登录功能实现:从"简单验证"到"安全防护"

3.1 我的第一版登录代码
java
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {
// 1. 查询用户
Employee emp = employeeMapper.selectByUsername(employee.getUsername());
// 2. 验证密码
if (emp != null && emp.getPassword().equals(employee.getPassword())) {
// 3. 存入Session
request.getSession().setAttribute("employee", emp.getId());
return R.success(emp);
}
return R.error("登录失败");
}
问题一堆:
- SQL注入风险:直接拼接SQL查询
- 密码明文传输:前端传明文,后端比较明文
- 没有异常处理:查询失败会直接抛异常
- 没有账户状态检查:禁用的账户也能登录
- 没有登录日志:无法追溯登录行为
3.2 生产级的登录实现
java
@Service
@Slf4j
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
@Autowired
private LoginLogService loginLogService; // 登录日志服务
@Override
public R<Employee> login(HttpServletRequest request, Employee employee) {
String username = employee.getUsername();
String password = employee.getPassword();
log.info("用户尝试登录: {}", username);
// 1. 参数校验
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
log.warn("登录参数为空: username={}", username);
return R.error("用户名或密码不能为空");
}
// 2. 构建查询条件(防SQL注入)
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername, username);
Employee emp = null;
try {
// 3. 查询用户(这里用MyBatis-Plus的getOne,确保只查一条)
emp = this.getOne(queryWrapper);
} catch (Exception e) {
log.error("查询用户失败: {}, error: {}", username, e.getMessage());
return R.error("系统异常,请稍后重试");
}
// 4. 用户不存在
if (emp == null) {
log.warn("用户不存在: {}", username);
// 记录登录失败日志(即使失败也要记录)
loginLogService.saveLoginLog(username, "用户不存在", false);
return R.error("用户名或密码错误");
}
// 5. 密码加密比较
String md5Password = DigestUtils.md5DigestAsHex(password.getBytes());
if (!emp.getPassword().equals(md5Password)) {
log.warn("密码错误: {}", username);
loginLogService.saveLoginLog(username, "密码错误", false);
return R.error("用户名或密码错误");
}
// 6. 检查账户状态
if (emp.getStatus() == 0) {
log.warn("账户被禁用: {}", username);
loginLogService.saveLoginLog(username, "账户被禁用", false);
return R.error("账户已被禁用");
}
// 7. 登录成功,记录日志
log.info("用户登录成功: {}", username);
loginLogService.saveLoginLog(username, "登录成功", true);
// 8. 保存登录状态到Session
request.getSession().setAttribute("employee", emp.getId());
// 9. 返回用户信息(脱敏处理)
EmployeeVO employeeVO = new EmployeeVO();
BeanUtils.copyProperties(emp, employeeVO);
// 脱敏处理
employeeVO.setPassword(null);
employeeVO.setIdNumber(maskIdNumber(emp.getIdNumber()));
return R.success(employeeVO);
}
private String maskIdNumber(String idNumber) {
if (StringUtils.isEmpty(idNumber) || idNumber.length() < 8) {
return idNumber;
}
// 身份证号脱敏:保留前4位和后4位
return idNumber.substring(0, 4) + "********" + idNumber.substring(idNumber.length() - 4);
}
}
四、全局异常处理:从"红屏报错"到"优雅提示"
4.1 那个让用户恐慌的500错误页面
用户反馈:"我点了提交,页面变成了白底红字,全是英文,吓死我了!"
我的代码没有任何异常处理,Spring Boot的默认错误页面直接暴露给了用户。
4.2 全局异常处理器
java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public R<String> handleBusinessException(BusinessException e) {
log.error("业务异常: {}", e.getMessage(), e);
return R.error(e.getMessage());
}
/**
* 处理SQL异常
*/
@ExceptionHandler(SQLException.class)
public R<String> handleSQLException(SQLException e) {
log.error("SQL异常: {}", e.getMessage(), e);
// 判断是否为唯一约束冲突
if (e.getMessage().contains("Duplicate entry")) {
String[] split = e.getMessage().split(" ");
String value = split[2].replace("'", "");
return R.error(value + "已存在");
}
return R.error("数据库操作失败");
}
/**
* 处理参数验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public R<String> handleValidationException(MethodArgumentNotValidException e) {
log.error("参数验证异常: {}", e.getMessage());
// 提取所有错误信息
List<String> errors = e.getBindingResult()
.getFieldErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
return R.error(String.join(", ", errors));
}
/**
* 处理系统异常
*/
@ExceptionHandler(Exception.class)
public R<String> handleException(Exception e) {
log.error("系统异常: ", e);
// 生产环境返回友好提示,不暴露具体错误
if (isProduction()) {
return R.error("系统异常,请稍后重试");
}
// 开发环境返回详细错误
return R.error("系统异常: " + e.getMessage());
}
private boolean isProduction() {
String env = System.getProperty("spring.profiles.active");
return "prod".equals(env);
}
}
4.3 自定义业务异常
java
public class BusinessException extends RuntimeException {
private Integer code;
public BusinessException(String message) {
super(message);
this.code = 500; // 默认错误码
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
this.code = 500;
}
public Integer getCode() {
return code;
}
}
五、统一返回格式:从"随心所欲"到"标准统一"
5.1 我之前的混乱返回
java
// Controller 1
@GetMapping("/list")
public List<Employee> list() {
return employeeService.list();
}
// Controller 2
@PostMapping("/add")
public String add(@RequestBody Employee employee) {
employeeService.save(employee);
return "success";
}
// Controller 3
@PutMapping("/update")
public boolean update(@RequestBody Employee employee) {
return employeeService.updateById(employee);
}
问题:前端需要为每个接口写不同的处理逻辑。
5.2 统一返回格式
java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
// 状态码
private Integer code;
// 消息
private String msg;
// 数据
private T data;
// 时间戳
private Long timestamp;
// 成功响应
public static <T> R<T> success() {
return success(null);
}
public static <T> R<T> success(T data) {
return R.<T>builder()
.code(200)
.msg("success")
.data(data)
.timestamp(System.currentTimeMillis())
.build();
}
public static <T> R<T> success(String msg, T data) {
return R.<T>builder()
.code(200)
.msg(msg)
.data(data)
.timestamp(System.currentTimeMillis())
.build();
}
// 错误响应
public static <T> R<T> error(String msg) {
return error(500, msg);
}
public static <T> R<T> error(Integer code, String msg) {
return R.<T>builder()
.code(code)
.msg(msg)
.data(null)
.timestamp(System.currentTimeMillis())
.build();
}
// 快捷方法
public static <T> R<T> ok() {
return success();
}
public static <T> R<T> ok(T data) {
return success(data);
}
public static <T> R<T> ok(String msg, T data) {
return success(msg, data);
}
// 判断是否成功
public boolean isSuccess() {
return code != null && code == 200;
}
}
使用方式:
java
@GetMapping("/{id}")
public R<EmployeeVO> getById(@PathVariable Long id) {
Employee employee = employeeService.getById(id);
if (employee == null) {
return R.error(404, "员工不存在");
}
EmployeeVO vo = convertToVO(employee);
return R.success(vo);
}
@PostMapping
public R<String> add(@RequestBody @Valid EmployeeDTO dto) {
employeeService.add(dto);
return R.success("添加成功");
}
六、分页查询:从"全量加载"到"性能优化"
6.1 我写的"性能杀手"
java
@GetMapping("/list")
public List<Employee> list() {
// 查所有数据!如果表里有100万条记录...
return employeeService.list();
}
6.2 使用MyBatis-Plus分页
java
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor();
paginationInterceptor.setDbType(DbType.MYSQL);
paginationInterceptor.setMaxLimit(1000L); // 单页最大1000条
paginationInterceptor.setOverflow(true); // 超过总页数后返回第一页
interceptor.addInnerInterceptor(paginationInterceptor);
// 乐观锁插件(可选)
OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor = new OptimisticLockerInnerInterceptor();
interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor);
return interceptor;
}
}
java
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee>
implements EmployeeService {
@Override
public R<PageResult<EmployeeVO>> pageQuery(EmployeeQueryDTO queryDTO) {
// 1. 构建分页条件
Page<Employee> page = new Page<>(queryDTO.getPage(), queryDTO.getSize());
// 2. 构建查询条件
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
// 姓名模糊查询
if (StringUtils.isNotBlank(queryDTO.getName())) {
queryWrapper.like(Employee::getName, queryDTO.getName());
}
// 用户名精确查询
if (StringUtils.isNotBlank(queryDTO.getUsername())) {
queryWrapper.eq(Employee::getUsername, queryDTO.getUsername());
}
// 状态查询
if (queryDTO.getStatus() != null) {
queryWrapper.eq(Employee::getStatus, queryDTO.getStatus());
}
// 创建时间范围查询
if (queryDTO.getBeginTime() != null && queryDTO.getEndTime() != null) {
queryWrapper.between(Employee::getCreateTime,
queryDTO.getBeginTime(),
queryDTO.getEndTime());
}
// 排序
queryWrapper.orderByDesc(Employee::getUpdateTime);
// 3. 执行分页查询
Page<Employee> employeePage = this.page(page, queryWrapper);
// 4. 转换为VO
List<EmployeeVO> voList = employeePage.getRecords().stream()
.map(this::convertToVO)
.collect(Collectors.toList());
// 5. 构建分页结果
PageResult<EmployeeVO> pageResult = PageResult.<EmployeeVO>builder()
.list(voList)
.total(employeePage.getTotal())
.page(queryDTO.getPage())
.size(queryDTO.getSize())
.pages(employeePage.getPages())
.build();
return R.success(pageResult);
}
private EmployeeVO convertToVO(Employee employee) {
EmployeeVO vo = new EmployeeVO();
BeanUtils.copyProperties(employee, vo);
// 脱敏处理
vo.setPassword("******");
vo.setIdNumber(maskIdNumber(employee.getIdNumber()));
vo.setPhone(maskPhone(employee.getPhone()));
return vo;
}
private String maskPhone(String phone) {
if (StringUtils.isEmpty(phone) || phone.length() < 7) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
}
七、项目部署:从"本地王者"到"生产乞丐"
7.1 我的第一次部署尝试
- 把jar包用QQ发给运维
- "你帮我放到服务器上,运行一下"
- 然后就没有然后了...
7.2 完整的部署流程

7.2.1 服务器准备
bash
#!/bin/bash
# server-init.sh
echo "=== 开始初始化服务器 ==="
# 1. 安装JDK
echo "1. 安装JDK 11..."
yum install -y java-11-openjdk-devel
# 2. 安装MySQL
echo "2. 安装MySQL 8.0..."
wget https://dev.mysql.com/get/mysql80-community-release-el7-7.noarch.rpm
rpm -ivh mysql80-community-release-el7-7.noarch.rpm
yum install -y mysql-community-server
systemctl start mysqld
systemctl enable mysqld
# 3. 初始化数据库
echo "3. 初始化数据库..."
mysql -e "CREATE DATABASE IF NOT EXISTS reggie DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# 4. 安装Nginx
echo "4. 安装Nginx..."
yum install -y nginx
systemctl start nginx
systemctl enable nginx
echo "=== 服务器初始化完成 ==="
7.2.2 应用部署脚本
bash
#!/bin/bash
# deploy.sh
set -e
APP_NAME="reggie"
APP_PORT=8080
BACKUP_DIR="/opt/backup/$(date +%Y%m%d_%H%M%S)"
DEPLOY_DIR="/opt/$APP_NAME"
echo "=== 开始部署 $APP_NAME ==="
# 1. 停止旧服务
echo "1. 停止旧服务..."
systemctl stop $APP_NAME.service 2>/dev/null || true
# 2. 备份旧版本
echo "2. 备份旧版本..."
mkdir -p $BACKUP_DIR
cp -r $DEPLOY_DIR/* $BACKUP_DIR/ 2>/dev/null || true
# 3. 清理部署目录
echo "3. 清理部署目录..."
rm -rf $DEPLOY_DIR/*
mkdir -p $DEPLOY_DIR/{app,logs,config}
# 4. 复制新版本
echo "4. 复制新版本..."
cp target/*.jar $DEPLOY_DIR/app/$APP_NAME.jar
cp -r src/main/resources/static $DEPLOY_DIR/app/
cp deploy/application-prod.yml $DEPLOY_DIR/config/application.yml
# 5. 创建服务文件
echo "5. 创建服务文件..."
cat > /etc/systemd/system/$APP_NAME.service << EOF
[Unit]
Description=$APP_NAME Service
After=network.target
[Service]
Type=simple
User=nobody
WorkingDirectory=$DEPLOY_DIR/app
ExecStart=/usr/bin/java -jar $APP_NAME.jar --spring.config.location=../config/application.yml
Restart=always
RestartSec=10
StandardOutput=append:$DEPLOY_DIR/logs/app.log
StandardError=append:$DEPLOY_DIR/logs/error.log
[Install]
WantedBy=multi-user.target
EOF
# 6. 设置权限
echo "6. 设置权限..."
chown -R nobody:nobody $DEPLOY_DIR
chmod 755 $DEPLOY_DIR/app/$APP_NAME.jar
# 7. 启动服务
echo "7. 启动服务..."
systemctl daemon-reload
systemctl start $APP_NAME.service
systemctl enable $APP_NAME.service
# 8. 检查服务状态
echo "8. 检查服务状态..."
sleep 5
if systemctl is-active --quiet $APP_NAME.service; then
echo "✅ 服务启动成功"
# 检查端口是否监听
if netstat -tlnp | grep ":$APP_PORT" > /dev/null; then
echo "✅ 端口 $APP_PORT 监听正常"
else
echo "❌ 端口 $APP_PORT 未监听"
exit 1
fi
else
echo "❌ 服务启动失败"
echo "查看日志: tail -f $DEPLOY_DIR/logs/error.log"
exit 1
fi
echo "=== 部署完成 ==="
7.2.3 Nginx配置
nginx
# /etc/nginx/conf.d/reggie.conf upstream reggie_backend { server 127.0.0.1:8080; # 如果有多个实例可以做负载均衡 # server 127.0.0.1:8081; } server { listen 80; server_name your-domain.com; # 前端页面 location / { root /opt/reggie/app/static; index index.html; # 解决Vue/React路由刷新404 try_files $uri $uri/ /index.html; # 缓存静态资源 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control "public, immutable"; } } # 后端API location /api/ { proxy_pass http://reggie_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 超时设置 proxy_connect_timeout 30s; proxy_send_timeout 30s; proxy_read_timeout 30s; # 文件上传大小限制 client_max_body_size 10m; } }
八、监控和日志:从"瞎子摸象"到"明察秋毫"
8.1 健康检查接口
java
@RestController
@RequestMapping("/actuator")
@Slf4j
public class HealthController {
@Autowired
private DataSource dataSource;
@GetMapping("/health")
public Map<String, Object> health() {
Map<String, Object> result = new HashMap<>();
// 应用状态
result.put("status", "UP");
result.put("timestamp", System.currentTimeMillis());
// 数据库状态
Map<String, Object> dbStatus = new HashMap<>();
try {
Connection connection = dataSource.getConnection();
boolean valid = connection.isValid(2); // 2秒超时
connection.close();
dbStatus.put("status", valid ? "UP" : "DOWN");
} catch (Exception e) {
dbStatus.put("status", "DOWN");
dbStatus.put("error", e.getMessage());
log.error("数据库健康检查失败", e);
}
result.put("database", dbStatus);
// 系统信息
Runtime runtime = Runtime.getRuntime();
Map<String, Object> systemInfo = new HashMap<>();
systemInfo.put("memory_used", (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024);
systemInfo.put("memory_total", runtime.totalMemory() / 1024 / 1024);
systemInfo.put("memory_max", runtime.maxMemory() / 1024 / 1024);
systemInfo.put("cpu_cores", runtime.availableProcessors());
systemInfo.put("uptime", ManagementFactory.getRuntimeMXBean().getUptime());
result.put("system", systemInfo);
return result;
}
}
8.2 日志配置
yaml
# application-prod.yml
logging:
level:
root: INFO
com.reggie: DEBUG
org.springframework.web: WARN
org.hibernate: WARN
# 文件输出
file:
name: logs/application.log
# 日志格式
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
# 日志切割
logback:
rollingpolicy:
max-file-size: 10MB
max-history: 30
total-size-cap: 1GB
九、经验总结:从"会写代码"到"会做项目"
9.1 我踩过的坑
- 环境不一致:本地能跑,服务器不能跑
- 权限问题:文件权限、数据库权限、服务权限
- 资源不足:内存不足、磁盘不足、连接数不足
- 配置错误:端口冲突、路径错误、依赖缺失
- 监控缺失:不知道系统状态,出问题只能猜
9.2 我的最佳实践
- 统一配置:使用配置中心管理所有配置
- 容器化部署:使用Docker保证环境一致
- 自动化部署:CI/CD流水线
- 完善监控:应用监控、业务监控、日志监控
- 健全告警:异常告警、性能告警、安全告警
9.3 如果重来一次
我会:
- 从一开始就考虑部署和运维
- 建立完善的监控体系
- 写详细的文档和脚本
- 设计可扩展的架构
- 重视安全和性能
结语:从程序员到工程师
从那个对着空白屏幕绝望的新手,到现在能从容处理线上故障的工程师,我最大的感悟是:
技术只是工具,解决问题才是目的。
一个好的工程师,不仅要会写代码,还要:
- 理解业务需求
- 设计合理架构
- 考虑运维成本
- 重视用户体验
- 持续学习改进
记住:代码的质量,决定了你加班的时间。架构的合理性,决定了项目的寿命。
知识点测试
读完文章了?来测试一下你对知识点的掌握程度吧!
评论区
使用 GitHub 账号登录后即可发表评论,支持 Markdown 格式。
如果评论系统无法加载,请确保:
- 您的网络可以访问 GitHub
- giscus GitHub App 已安装到仓库
- 仓库已启用 Discussions 功能