Spring风景线
- Spring核心框架:Spring MVC,数据持久化支持,反应式编程等
- Spring Boot:starter,Actuator,环境属性规范等
- Spring Data:数据映射、多数据源支持
- Spring Security:身份验证、授权和API安全性等
- Spring Integration 和 Spring Batch:实时集成和批处理
- Spring Cloud:微服务
chap2 开发Web应用
Spring 项目结构
- /src/main/java 项目源码
- /src/main/resources 非Java的资源
- static 静态内容
- templates 渲染内容的模版,如 Thymeleaf
- /src/test/java 测试代码
测试Web应用
@RunWith(SpringRunner.class)是JUnit提供的注解。提供测试运行器知道JUnit运行测试。SpringRunner是SpringJUnit4ClassRunner的别名- SpringMVC 是 Spring 自带的Web框架。使用
@WebMvcTest(HomeController.class)可以测试Web应用
@RunWith(SpringRunner.class)
@WebMvcTest(HomeController.class) // 让测试在Spring MVC的上下文中执行
public class HomeControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testHomePage() throws Exception {
mockMvc.perform(get("/")) // 请求路径
.andExpect(status().isOk()) // 状态码断言
.andExpect(view().name("home")) // 视图断言
.andExpect(content().string( // 视图内容断言
containsString("Welcome to...")));
}
}
校验表单输入
Spring Boot 中包含了 Hibernate 作为 Validation API 的实现,可以支持Bean校验
/**
使用方式:
1. 被校验的类中声明规则
2. 在controller中声明需要进行校验 @Valid
3. 展示校验错误
*/
@Data
public class Taco {
@NotNull
@Size(min=5, message="Name must be at least 5 characters long")
private String name;
@Size(min=1, message="You must choose at least 1 ingredient")
private List<String> ingredients;
}
@Slf4j
@Controller
@RequestMapping("/design")
public class DesignTacoController {
@PostMapping
public String processDesign(@Valid Taco design, Errors errors, Model model) {
if (errors.hasErrors()) {
return "design";
}
log.info("Processing design: " + design);
return "redirect:/orders/current";
}
}
chap3 数据持久化
JdbcTemplate
- 传统的JDBC使用方式
public class RawJdbcIngredientRepository implements IngredientRepository {
private DataSource dataSource;
public RawJdbcIngredientRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Iterable<Ingredient> findAll() {
List<Ingredient> ingredients = new ArrayList<>();
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connection = dataSource.getConnection(); // 1-获取connection
statement = connection.prepareStatement( // 2-构建SQL语句
"select id, name, type from Ingredient");
resultSet = statement.executeQuery(); // 3-执行查询
while(resultSet.next()) { // 4-解析结果
Ingredient ingredient = new Ingredient(
resultSet.getString("id"),
resultSet.getString("name"),
Ingredient.Type.valueOf(resultSet.getString("type")));
ingredients.add(ingredient);
}
} catch (SQLException e) {
// SQLException是一个检查型异常,必须catch处理
// 可能出现两类错误:数据库连接失败或SQL语句异常
} finally {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {}
}
}
return ingredients;
}
}
- 使用JdbcTemplate
@Repository
public class JdbcIngredientRepository implements IngredientRepository {
private JdbcTemplate jdbc;
@Autowired
public JdbcIngredientRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
/**
query() 方法入参:1-sql,2-行数据映射类Spring RowMapper的实现
*/
@Override
public Iterable<Ingredient> findAll() {
return jdbc.query("select id, name, type from Ingredient",
this::mapRowToIngredient);
}
private Ingredient mapRowToIngredient(ResultSet rs, int rowNum)
throws SQLException {
return new Ingredient(
rs.getString("id"),
rs.getString("name"),
Ingredient.Type.valueOf(rs.getString("type")));
}
}
- 应用启动时会自动读取并执行
/src/main/resources下的sql文件- schema.sql 一般定影数据表结构
- data.sql 用来插入数据
./src/main/resources
├── application.yml
├── data.sql
├── schema.sql
├── static
│ ├── images
│ │ └── TacoCloud.png
│ └── styles.css
└── templates
├── design.html
├── home.html
└── orderForm.html
- 插入数据:update() 方法 或 SimpleJdbcInsert
// 普通 update 方法
private void saveIngredientToTaco(
Ingredient ingredient, long tacoId) {
jdbc.update(
"insert into Taco_Ingredients (taco, ingredient) " +
"values (?, ?)",
tacoId, ingredient.getId());
}
// 返回主键的 update 方法
private long saveTacoInfo(Taco taco) {
taco.setCreatedAt(new Date());
PreparedStatementCreator psc =
new PreparedStatementCreatorFactory(
"insert into Taco (name, createdAt) values (?, ?)",
Types.VARCHAR, Types.TIMESTAMP
).newPreparedStatementCreator(
Arrays.asList(
taco.getName(),
new Timestamp(taco.getCreatedAt().getTime())));
KeyHolder keyHolder = new GeneratedKeyHolder();
// 需要参数:PreparedStatementCreator 和 keyHolder
jdbc.update(psc, keyHolder);
return keyHolder.getKey().longValue();
}
- 插入数据:SimpleJdbcInsert
@Repository
public class JdbcOrderRepository implements OrderRepository {
private SimpleJdbcInsert orderInserter;
private SimpleJdbcInsert orderTacoInserter;
private ObjectMapper objectMapper;
@Autowired
public JdbcOrderRepository(JdbcTemplate jdbc) {
// 创建SimpleJdbcInsert
this.orderInserter = new SimpleJdbcInsert(jdbc)
.withTableName("Taco_Order")
.usingGeneratedKeyColumns("id");
// 创建SimpleJdbcInsert
this.orderTacoInserter = new SimpleJdbcInsert(jdbc)
.withTableName("Taco_Order_Tacos");
// 将对象映射为K-V形式
this.objectMapper = new ObjectMapper();
}
@Override
public Order save(Order order) {
order.setPlacedAt(new Date());
long orderId = saveOrderDetails(order);
order.setId(orderId);
List<Taco> tacos = order.getTacos();
for (Taco taco : tacos) {
saveTacoToOrder(taco, orderId);
}
return order;
}
private long saveOrderDetails(Order order) {
// Bean转Map
@SuppressWarnings("unchecked")
Map<String, Object> values =
objectMapper.convertValue(order, Map.class);
values.put("placedAt", order.getPlacedAt());
long orderId =
orderInserter
.executeAndReturnKey(values) // 插入并获取主键
.longValue();
return orderId;
}
private void saveTacoToOrder(Taco taco, long orderId) {
Map<String, Object> values = new HashMap<>();
values.put("tacoOrder", orderId);
values.put("taco", taco.getId());
orderTacoInserter.execute(values); // 简单插入
}
Spring Data JPA
使用步骤:
1-添加Spring Data JPA依赖
2-将领域对象标注为实体 (@Entity, @Id)
3-声明和自定义 JPA Repo
将领域对象标注为实体 (@Entity, @Id)
// Taco 和 Ingredient 是多对多关系
@Data
@RequiredArgsConstructor
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@Entity // JPA
public class Ingredient {
@Id // JPA
private final String id;
private final String name;
private final Type type;
public static enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}
@Data
@Entity
@Table(name="Taco") // Taco实体对应的数据库实体为Taco表
public class Taco {
@Id
@GeneratedValue(strategy=GenerationType.AUTO) // 自增主键
private Long id;
@NotNull
@Size(min=5, message="Name must be at least 5 characters long")
private String name;
private Date createdAt;
@ManyToMany(targetEntity=Ingredient.class) // JPA多对多关系
@Size(min=1, message="You must choose at least 1 ingredient")
private List<Ingredient> ingredients = new ArrayList<>();
@PrePersist // 持久化之前会将createdAt设置为当前时间
void createdAt() {
this.createdAt = new Date();
}
}
声明 JPA Repo
// 继承CrudRepository即可。在运行时会自动生成实现类并提供通用方法
// CrudRepository<实体化类型, 主键ID类型>
public interface TacoRepository
extends CrudRepository<Taco, Long> {
}
自定义 JPA Repo
- SpringData自定义了DSL用于解析repository方法的签名
- @Query 注解可以声明SQL
// 动词+by+属性+条件
List<Order> readOrdersByDeliveryZipAndPlacedAtBetween(String deliveryZip, Date startDate, Date endDate);
// @Query注解自定义SQL
@Query("Order o where o.deliveryCity='Seattle'")
List<Order> readOrdersDeliveredInSeattle();
chap4 Spring Security
引入 Spring Security 的starter依赖后,默认用户名user,在日志文件中会写入随机生成的密码,如
Using default security password: xxx-xxxx-xxxx
Spring Security 提供基于内存、JDBC、LDAP和自定义的用户存储
配置 Spring Security
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
// ...
}
}
基于JDBC的用户存储
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource);
}
}
-
Spring Security 内置一组查询用户信息所执行的默认SQL。对应着Spring Security预设的一套数据库表结构,包括
users、authorities、groups、group_members和group_authorities -
从
org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl中可以获取到鉴权相关的默认SQL -
也可以自定义查询内容
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"select username, password, enabled from Users " +
"where username=?")
.authoritiesByUsernameQuery(
"select username, authority from UserAuthorities " +
"where username=?");
// 指定passwordEncoder避免密码明文传输
.passwordEncoder(new StandardPasswordEncoder("53cr3t");
}
}
自定义用户认证
- 实现UserDetails接口,定义用户领域持久化对象
- 实现UserDetailService,实现 loadUserByUsername 方法,提供service
鉴权和登出
- 重写WebSecurityConfigurerAdapter中的configure(HttpSecurity http) 方法
@SuppressWarnings("deprecation")
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() // 会按顺序进行校验
.antMatchers("/design", "/orders")
.hasRole("ROLE_USER")
.antMatchers("/", "/**").access("permitAll")
.and()
.formLogin()
.loginPage("/login") // 自定义登录页
.and()
.logout()
.logoutSuccessUrl("/") // 登出后重定向
.and()
.csrf()
.ignoringAntMatchers("/h2-console/**")
// Allow pages to be loaded in frames from the same origin; needed for H2-Console
// tag::frameOptionsSameOrigin[]
.and()
.headers()
.frameOptions()
.sameOrigin()
;
}
}
- 除了使用默认鉴权方法外,还可以使用 access() + SpEL 声明安全规则
http
.authorizeRequests()
.antMatchers("/design", "/orders")
.access("hasRole('ROLE_USER')")
获取用户信息
多种方式:
- 注入 Principal 对象:controller可用
- 注入 Authentication 对象:controller可用
- 使用 SecurityContextHolder:随处可用但是麻烦
- 使用 @AuthenticationPrincipal 注解:controller方法入参
配置属性
Spring提供两种配置:
- Bean装配:声明在Spring容器中创建的组件和组件之间的注入关系
- 属性注入:配置Bean的属性
Spring中配置的Bean可用通过Spring环境提取属性值
自定义配置属性
在类上添加 @ConfigurationProperties 注解后可用为当前Bean提供配置的属性值
@ConfigurationProperties 注解通常会放到专门用来配置属性的Bean中
@Component // 配置类
@ConfigurationProperties(prefix="taco.order") // 读取配置属性
@Data
@Validated // 开启校验
public class OrderCodeProps {
@Min()
@Max()
private int pageSize = 20; // 配置属性
}
// 使用配置类
@Controller
@ReqeustMapping
public class OrderController {
// 注入配置类
private OrderProps props;
// 读取配置
// props.getPageSize
}
配置文件 application.yaml
taco:
orders:
pageSize: 10
- 声明自定义配置属性的元数据
避免出现如 Unknown property xxx 的告警提示
在 META-INF 下创建 additional-spring-configuration-metadata.json 的文件并添加配置:
{"properties": [
{
"name": "taco.orders.page-size",
"type": "java.lang.String",
"description": "Sets the maximum number of orders to display in a list."
}
]}
使用 profile
创建属于不同环境的profile的方式:
- 创建不同环境下的profile文件
application-{profile}.yml,如 application-prod.yml - 或在同一个yaml文件中使用
spring.profiles属性隔离不同环境间的配置,如:
taco:
orders:
pageSize: 10
---
spring:
profiles: prod
datasource:
url: jdbc:mysql://localhost/tacocloud
username: tacouser
password: tacopassword
logging:
level:
tacos: WARN
激活profile:
推荐使用环境变量设置处于激活状态的profile,如
export SPRING_PROFILES_ACTIVE=prod
使用@Profile()注解根据不同profile创建Bean
@Profile("!prod") // 非生产环境才需要装配该bean
@Configuration
public class DevelopmentConfig {
@Bean
public CommandLineRunner dataLoader(IngredientRepository repo,
UserRepository userRepo, PasswordEncoder encoder) { // user repo for ease of testing with a built-in user
return new CommandLineRunner() {
@Override
public void run(String... args) throws Exception {
repo.save(new Ingredient("FLTO", "Flour Tortilla", Type.WRAP));
// ...
};
}
}